2827 lines
95 KiB
GDScript

# tabletop-club
# Copyright (c) 2020-2024 Benjamin 'drwhut' Beddows.
# Copyright (c) 2021-2024 Tabletop Club contributors (see game/CREDITS.tres).
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
extends Spatial
signal setting_spawn_point(position)
signal spawning_piece_at(position)
signal spawning_piece_in_container(container_name)
signal table_flipped()
signal table_unflipped()
signal undo_stack_empty()
signal undo_stack_pushed()
onready var _camera_controller = $CameraController
onready var _fast_circle = $FastCircle
onready var _hand_positions = $Table/HandPositions
onready var _hands = $Hands
onready var _hidden_areas = $HiddenAreas
onready var _hidden_area_preview = $HiddenAreaPreview
onready var _pieces = $Pieces
onready var _spot_light = $SpotLight
onready var _sun_light = $SunLight
onready var _table = $Table
onready var _world_environment = $WorldEnvironment
const HIDDEN_AREA_MIN_SIZE = Vector2(0.5, 0.5)
const LIMBO_DURATION_MS = 3000
const UNDO_STACK_SIZE_LIMIT = 20
const UNDO_STATE_EVENT_TIMEOUTS_MS = {
"add_piece": 10000,
"add_piece_to_container": 10000,
"add_stack_filled": 10000,
"flip_table": 0,
"place_hidden_area": 10000,
"remove_hidden_area": 5000,
"remove_piece_from_container": 5000,
"remove_pieces": 5000,
"request_collect_pieces": 5000,
"request_hover_piece": 5000,
"request_pop_stack": 5000,
"request_stack_collect_all": 5000,
"set_state": 0,
"set_table": 5000,
}
var _srv_allow_card_stacking = true
var _srv_hand_setup_frames = -1
var _srv_next_piece_name = 0
var _srv_retrieve_pieces_from_hell = true
var _srv_undo_stack: Array = []
# This allows for functions to stop the creation of undo states when calling
# e.g. add_piece.
var _srv_undo_disable_state_creation: int = 0
var _srv_undo_state_last_call_ms: Dictionary = {}
# If clients start hovering multiple pieces at a time, then keep track here of
# which pieces they hover, so that the client doesn't have to send multiple
# requests with the same position.
var _client_hover_pieces: Dictionary = {}
var _table_body: RigidBody = null
var _paint_plane = preload("res://Scenes/Game/3D/PaintPlane.tscn").instance()
# Add a hand to the game for a given player.
# player: The ID of the player the hand should belong to.
# transform: The transform of the new hand.
remotesync func add_hand(player: int, transform: Transform) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var hand = preload("res://Scenes/Game/3D/Hand.tscn").instance()
hand.name = str(player)
hand.transform = transform
_hands.add_child(hand)
hand.update_owner_display()
# Called by the server to add a piece to the room.
# name: The name of the new piece.
# transform: The initial transform of the new piece.
# entry_path: The piece's entry path in the AssetDB.
remotesync func add_piece(name: String, transform: Transform,
entry_path: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var piece_entry = AssetDB.search_path(entry_path)
if piece_entry.empty():
push_error("Cannot add piece, entry not found: %s" % entry_path)
return
if get_tree().is_network_server():
push_undo_state("add_piece")
var piece: Piece = null
if PieceCache.should_cache(piece_entry):
var piece_cache = PieceCache.new(entry_path, false)
var maybe_piece = piece_cache.get_scene()
if maybe_piece != null and maybe_piece is Piece:
piece = maybe_piece
else:
if maybe_piece != null:
ResourceManager.free_object(maybe_piece)
piece = PieceBuilder.build_piece(piece_entry)
else:
piece = PieceBuilder.build_piece(piece_entry)
piece.name = name
piece.transform = transform
if get_tree().is_network_server():
piece.srv_retrieve_from_hell = _srv_retrieve_pieces_from_hell
# If it is a stackable piece, make sure we attach the signal it emits when
# it wants to create a stack.
if piece is StackablePiece:
piece.connect("stack_requested", self, "_on_stack_requested")
# If it is a container, make sure we attach the signal it emits when it
# wants to absorb or release a piece.
if piece is PieceContainer:
piece.connect("absorbing_hovered", self, "_on_container_absorbing_hovered")
piece.connect("releasing_random_piece", self, "_on_container_releasing_random_piece")
_pieces.add_child(piece)
# Called by the server to add a piece to a container, a.k.a. having the piece
# be "absorbed" by the container.
# container_name: The name of the container that is absorbing the piece.
# piece_name: The name of the piece that the container is absorbing.
remotesync func add_piece_to_container(container_name: String, piece_name: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var container = _pieces.get_node(container_name)
var piece = _pieces.get_node(piece_name)
if not container:
push_error("Container " + container_name + " does not exist!")
return
if not piece:
push_error("Piece " + piece_name + " does not exist!")
return
if not container is PieceContainer:
push_error("Piece " + container_name + " is not a container!")
return
if not piece is Piece:
push_error("Object " + piece_name + " is not a piece!")
return
_camera_controller.remove_piece_ref(piece)
for player_id in _client_hover_pieces:
var hovering: Array = _client_hover_pieces[player_id]
hovering.erase(piece_name)
if get_tree().is_network_server():
push_undo_state("add_piece_to_container")
piece.stop_hovering()
_pieces.remove_child(piece)
container.add_piece(piece)
# If we are in multiplayer, then we'll need to put a piece into limbo with
# the same name, just in case an RPC is received afterwards.
if Lobby.get_player_count() > 1:
var limbo_piece = Piece.new()
limbo_piece.name = piece_name
_pieces.add_child(limbo_piece)
# Do not push an undo state for this... thing.
_srv_undo_state_creation_disable()
remove_pieces([piece_name])
_srv_undo_state_creation_enable()
# Called by the server to add a piece to a stack.
# piece_name: The name of the piece.
# stack_name: The name of the stack.
# piece_transform: The server's transform of the piece.
# stack_transform: The server's transform of the stack.
# on: Where to add the piece to in the stack.
# flip: Should the piece be flipped upon entering the stack?
remotesync func add_piece_to_stack(piece_name: String, stack_name: String,
piece_transform: Transform, stack_transform: Transform,
on: int = Stack.STACK_AUTO, flip: int = Stack.FLIP_AUTO) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var piece = _pieces.get_node(piece_name)
var stack = _pieces.get_node(stack_name)
if not piece:
push_error("Piece " + stack_name + " does not exist!")
return
if not stack:
push_error("Stack " + stack_name + " does not exist!")
return
if not piece is StackablePiece:
push_error("Piece " + piece_name + " is not stackable!")
return
if not stack is Stack:
push_error("Piece " + stack_name + " is not a stack!")
return
piece.transform = piece_transform
stack.transform = stack_transform
var piece_entry = piece.piece_entry
if piece.is_albedo_color_exposed():
piece_entry["color"] = piece.get_albedo_color()
# Don't add an undo state, since it would be saved just before the piece
# gets added to the stack.
_srv_undo_state_creation_disable()
remove_pieces([piece_name])
stack.add_piece(piece.piece_entry, piece.transform, on, flip)
_srv_undo_state_creation_enable()
# Called by the server to add a stack to the room with 2 initial pieces.
# name: The name of the new stack.
# piece1_name: The name of the first piece to add to the stack.
# piece2_name: The name of the second piece to add to the stack.
# piece1_transform: The server's transform of the first piece.
# piece2_transform: The server's transform of the second piece.
remotesync func add_stack(name: String, piece1_name: String, piece2_name: String,
piece1_transform: Transform, piece2_transform: Transform) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var piece1 = _pieces.get_node(piece1_name)
var piece2 = _pieces.get_node(piece2_name)
if not piece1:
push_error("Stackable piece " + piece1_name + " does not exist!")
return
if not piece2:
push_error("Stackable piece " + piece2_name + " does not exist!")
return
if not piece1 is StackablePiece:
push_error("Piece " + piece1_name + " is not stackable!")
return
if not piece2 is StackablePiece:
push_error("Piece " + piece2_name + " is not stackable!")
return
var sandwich_stack = (piece1.piece_entry["scene_path"] == "res://Pieces/Card.tscn")
var stack = add_stack_empty(name, piece2_transform, sandwich_stack)
var new_angular_velocity: Vector3 = piece1.angular_velocity
if piece2.angular_velocity.length_squared() < piece1.angular_velocity.length_squared():
new_angular_velocity = piece2.angular_velocity
var new_linear_velocity: Vector3 = piece1.linear_velocity
if piece2.linear_velocity.length_squared() < piece1.linear_velocity.length_squared():
new_angular_velocity = piece2.linear_velocity
stack.angular_velocity = new_angular_velocity
stack.linear_velocity = new_linear_velocity
add_piece_to_stack(piece1.name, name, piece1_transform, piece2_transform)
add_piece_to_stack(piece2.name, name, piece2_transform, piece2_transform)
# Called by the server to add an empty stack to the room.
# name: The name of the new stack.
# transform: The initial transform of the new stack.
# sandwich: If true, add a StackSandwich. If false, add a StackLasagne.
puppet func add_stack_empty(name: String, transform: Transform, sandwich: bool) -> Stack:
# Special case here, where we don't want the RPC to be sent to the server,
# but the server needs the stack to be returned.
if not (get_tree().is_network_server() or get_tree().get_rpc_sender_id() == 1):
return null
var stack: Stack = null
if sandwich:
stack = preload("res://Pieces/StackSandwich.tscn").instance()
else:
stack = preload("res://Pieces/StackLasagne.tscn").instance()
stack.name = name
stack.transform = transform
if get_tree().is_network_server():
stack.srv_retrieve_from_hell = _srv_retrieve_pieces_from_hell
if stack.effect_player_path.empty():
var effect_player = AudioStreamPlayer3D.new()
effect_player.name = "EffectPlayer"
effect_player.bus = "Effects"
effect_player.unit_size = 20
stack.add_child(effect_player)
stack.effect_player_path = NodePath("EffectPlayer")
_pieces.add_child(stack)
stack.connect("stack_requested", self, "_on_stack_requested")
return stack
# Called by the server to add a pre-filled stack to the room.
# name: The name of the new stack.
# transform: The initial transform of the new stack.
# stack_entry_path: The stack's entry path in the AssetDB.
remotesync func add_stack_filled(name: String, transform: Transform,
stack_entry_path: String) -> void:
var stack_entry = AssetDB.search_path(stack_entry_path)
if stack_entry.empty():
push_error("Cannot add stack, entry not found: %s" % stack_entry_path)
return
if get_tree().is_network_server():
push_undo_state("add_stack_filled")
var sandwich_stack = (stack_entry["scene_path"] == "res://Pieces/Card.tscn")
var stack = add_stack_empty(name, transform, sandwich_stack)
PieceBuilder.fill_stack(stack, stack_entry)
stack.transform.origin.y += stack.get_size().y / 2.0
# Called by the server to merge the contents of one stack into another stack.
# stack1_name: The name of the stack to merge contents from.
# stack2_name: The name of the stack to merge contents to.
# stack1_transform: The server's transform for the first stack.
# stack2_transform: The server's transform for the second stack.
remotesync func add_stack_to_stack(stack1_name: String, stack2_name: String,
stack1_transform: Transform, stack2_transform: Transform) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var stack1 = _pieces.get_node(stack1_name)
var stack2 = _pieces.get_node(stack2_name)
if not stack1:
push_error("Stack " + stack1_name + " does not exist!")
return
if not stack2:
push_error("Stack " + stack2_name + " does not exist!")
return
if not stack1 is Stack:
push_error("Piece " + stack1_name + " is not a stack!")
return
if not stack2 is Stack:
push_error("Piece " + stack2_name + " is not a stack!")
return
# If there are no children in the first stack, don't bother doing anything.
if stack1.empty():
return
stack1.transform = stack1_transform
stack2.transform = stack2_transform
# We need to determine in which order to add the children of the first stack
# to the second stack.
# NOTE: In stacks, children are stored bottom-first.
var reverse = false
if stack1.transform.origin.y > stack2.transform.origin.y:
reverse = stack1.transform.basis.y.y < 0
else:
reverse = stack1.transform.basis.y.y > 0
var pieces = stack1.get_pieces()
if reverse:
pieces.invert()
for piece_meta in pieces:
var piece_entry = piece_meta["piece_entry"]
var flip_y = piece_meta["flip_y"]
var piece_transform = stack1.transform
if flip_y:
var new_basis: Basis = piece_transform.basis
new_basis = new_basis.rotated(new_basis.z, PI)
piece_transform.basis = new_basis
stack2.add_piece(piece_entry, piece_transform)
# Don't push an undo state here, since it would be saved just before the
# first stack gets removed.
_srv_undo_state_creation_disable()
remove_pieces([stack1_name])
_srv_undo_state_creation_enable()
# Apply options from the options menu.
# config: The options to apply.
func apply_options(config: ConfigFile) -> void:
_camera_controller.apply_options(config)
var radiance = Sky.RADIANCE_SIZE_128
var radiance_index = config.get_value("video", "skybox_radiance_detail")
match radiance_index:
0:
radiance = Sky.RADIANCE_SIZE_128
1:
radiance = Sky.RADIANCE_SIZE_256
2:
radiance = Sky.RADIANCE_SIZE_512
3:
radiance = Sky.RADIANCE_SIZE_1024
4:
radiance = Sky.RADIANCE_SIZE_2048
_world_environment.environment.background_sky.radiance_size = radiance
var ssao_enabled = true
var ssao_quality = Environment.SSAO_QUALITY_LOW
var ssao_index = config.get_value("video", "ssao")
match ssao_index:
0:
ssao_enabled = false
1:
ssao_quality = Environment.SSAO_QUALITY_LOW
2:
ssao_quality = Environment.SSAO_QUALITY_MEDIUM
3:
ssao_quality = Environment.SSAO_QUALITY_HIGH
_world_environment.environment.ssao_enabled = ssao_enabled
_world_environment.environment.ssao_quality = ssao_quality
var dof_enabled = true
var dof_quality = Environment.DOF_BLUR_QUALITY_LOW
var dof_index = config.get_value("video", "depth_of_field")
match dof_index:
0:
dof_enabled = false
1:
dof_quality = Environment.DOF_BLUR_QUALITY_LOW
2:
dof_quality = Environment.DOF_BLUR_QUALITY_MEDIUM
3:
dof_quality = Environment.DOF_BLUR_QUALITY_HIGH
var dof_amount = 0.1 * config.get_value("video", "depth_of_field_amount")
var dof_distance = 15 + 85 * config.get_value("video", "depth_of_field_distance")
_world_environment.environment.dof_blur_far_amount = dof_amount
_world_environment.environment.dof_blur_far_distance = dof_distance
_world_environment.environment.dof_blur_far_enabled = dof_enabled
_world_environment.environment.dof_blur_far_quality = dof_quality
_world_environment.environment.dof_blur_far_transition = 10.0
_world_environment.environment.dof_blur_near_amount = dof_amount
_world_environment.environment.dof_blur_near_distance = 5.0
_world_environment.environment.dof_blur_near_enabled = dof_enabled
_world_environment.environment.dof_blur_near_quality = dof_quality
_world_environment.environment.dof_blur_near_transition = 1.0
var paint_filtering = config.get_value("video", "table_paint_filtering")
_paint_plane.set_filtering_enabled(paint_filtering)
# Compress the given room state.
# Returns: A dictionary, where "data" is the compressed version of the room
# state, and "size" is the size of the uncompressed data.
# state: The room state to compress.
func compress_state(state: Dictionary) -> Dictionary:
var bytes = var2bytes(state)
return {
"data": bytes.compress(File.COMPRESSION_FASTLZ),
"size": bytes.size()
}
# Flip the table.
# camera_basis: The basis matrix of the player flipping the table.
remotesync func flip_table(camera_basis: Basis) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
if _table_body == null:
return
if get_tree().is_network_server():
push_undo_state("flip_table")
# Unlock all pieces after we've saved the state so that the table doesn't
# get blocked.
for piece in _pieces.get_children():
if piece is Piece:
piece.unlock()
_table_body.mode = RigidBody.MODE_RIGID
var left = -camera_basis.x
var diagonal = -camera_basis.z
diagonal.y = 0.5
diagonal = diagonal.normalized()
_table_body.apply_central_impulse(_table_body.mass * 100 * diagonal)
_table_body.apply_torque_impulse(_table_body.mass * 2000 * left)
if get_tree().is_network_server():
srv_set_retrieve_pieces_from_hell(false)
emit_signal("table_flipped")
# Get the player camera's hover position.
# Returns: The current hover position.
func get_camera_hover_position() -> Vector3:
return _camera_controller.get_hover_position()
# Get the camera controller's transform.
# Returns: The camera controller's transform.
func get_camera_transform() -> Transform:
return _camera_controller.transform
# Get the color of the room lamp.
# Returns: The color of the lamp.
func get_lamp_color() -> Color:
if _sun_light.visible:
return _sun_light.light_color
else:
return _spot_light.light_color
# Get the intensity of the room lamp.
# Returns: The intensity of the lamp.
func get_lamp_intensity() -> float:
if _sun_light.visible:
return _sun_light.light_energy
else:
return _spot_light.light_energy
# Get the type of light the room lamp is emitting.
# Returns: True if the lamp is sunlight, false if it is a spotlight.
func get_lamp_type() -> bool:
return _sun_light.visible
# Get a piece in the room with a given name.
# Returns: The piece with the given name.
# name: The name of the piece.
func get_piece_with_name(name: String) -> Piece:
return _pieces.get_node(name)
# Get the list of pieces in the room.
# Returns: The list of pieces in the room.
func get_pieces() -> Array:
return _pieces.get_children()
# Get the number of pieces in the room.
# Returns: The number of pieces in the room.
func get_piece_count() -> int:
return _pieces.get_child_count()
# Get the current skybox's entry in the asset DB.
# Returns: The current skybox's entry, empty if it is using the default skybox.
func get_skybox() -> Dictionary:
if _world_environment.has_meta("skybox_entry"):
var skybox_entry = _world_environment.get_meta("skybox_entry")
if skybox_entry.has("texture_path"):
if not skybox_entry["texture_path"].empty():
return skybox_entry
return {}
# Get the current room state.
# Returns: The current room state.
# hands: Should the hand states be included?
# collisions: Should collision data be included?
func get_state(hands: bool = false, collisions: bool = false) -> Dictionary:
var out = {}
out["version"] = ProjectSettings.get_setting("application/config/version")
out["lamp"] = {
"color": get_lamp_color(),
"intensity": get_lamp_intensity(),
"sunlight": get_lamp_type()
}
var skybox_entry = get_skybox()
if skybox_entry.has("entry_path"):
out["skybox"] = skybox_entry["entry_path"]
# If the paint image is blank (it's default state), don't bother storing
# any image data in the state.
var paint_image = _paint_plane.get_paint()
var paint_image_data = null
if not paint_image.is_invisible():
paint_image_data = paint_image.get_data()
var table_entry = get_table()
if table_entry.has("entry_path"):
out["table"] = {
"entry_path": table_entry["entry_path"],
"is_rigid": false,
"paint_image_data": paint_image_data,
"transform": Transform.IDENTITY
}
if _table_body:
out["table"]["is_rigid"] = _table_body.mode == RigidBody.MODE_RIGID
out["table"]["transform"] = _table_body.transform
if hands:
var hand_dict = {}
for hand in _hands.get_children():
var hand_meta = {
"transform": hand.transform
}
hand_dict[hand.owner_id()] = hand_meta
out["hands"] = hand_dict
var hidden_area_dict = {}
for hidden_area in _hidden_areas.get_children():
if hidden_area is HiddenArea:
# Convert the transform of the hidden area to corner points so the
# set_state() function can re-use the function that creates the
# hidden area.
var area_origin = hidden_area.transform.origin
var area_scale = hidden_area.transform.basis.get_scale()
var point1_v3 = area_origin - area_scale
var point2_v3 = area_origin + area_scale
var hidden_area_meta = {
"player_id": hidden_area.player_id,
"point1": Vector2(point1_v3.x, point1_v3.z),
"point2": Vector2(point2_v3.x, point2_v3.z)
}
hidden_area_dict[hidden_area.name] = hidden_area_meta
out["hidden_areas"] = hidden_area_dict
_append_piece_states(out, _pieces.get_children(), collisions)
return out
# Get the current room state as a compressed byte array.
# Returns: A dictionary, where "data" is the compressed version of the room
# state, and "size" is the size of the uncompressed data.
# hands: Should the hand states be included?
# collisions: Should collision data be included?
func get_state_compressed(hands: bool = false, collisions: bool = false) -> Dictionary:
return compress_state(get_state(hands, collisions))
# Get the current table's entry in the asset DB.
# Returns: The current table's entry, empty if there is no table.
func get_table() -> Dictionary:
if _table_body:
if _table_body.has_meta("table_entry"):
return _table_body.get_meta("table_entry")
return {}
# Called by the server to paste the contents of a clipboard to the room.
# clipboard: The clipboard contents (from _append_piece_states).
# first_name: The name of the first piece to be created.
remotesync func paste_clipboard(clipboard: Dictionary, first_name: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
# _extract_piece_states() will use srv_get_next_piece_name() for all clients
# to determine the new piece names.
if not first_name.is_valid_integer():
push_error("First name %s is not a valid integer!" % first_name)
return
_srv_next_piece_name = int(first_name)
_extract_piece_states(clipboard, _pieces, true)
# Called by the server to place a hidden area for a given player.
# area_name: The name of the new hidden area.
# player_id: The player the hidden area is registered to.
# point1: One corner of the hidden area.
# point2: The opposite corner of the hidden area.
remotesync func place_hidden_area(area_name: String, player_id: int,
point1: Vector2, point2: Vector2) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
if get_tree().is_network_server():
push_undo_state("place_hidden_area")
var size = (point2 - point1).abs()
if size.x < HIDDEN_AREA_MIN_SIZE.x or size.y < HIDDEN_AREA_MIN_SIZE.y:
return
var hidden_area: HiddenArea = preload("res://Scenes/Game/3D/HiddenArea.tscn").instance()
hidden_area.name = area_name
hidden_area.player_id = player_id
_set_hidden_area_transform(hidden_area, point1, point2)
_hidden_areas.add_child(hidden_area)
hidden_area.update_player_color()
# Takes the last undo state from the stack and sets it for all players.
master func pop_undo_state() -> void:
if _srv_undo_stack.empty():
push_error("Cannot pop undo stack, is empty!")
return
# Reset all of the undo timers to -1 min, so that they are guaranteed to
# create states after this undo.
for func_name in _srv_undo_state_last_call_ms:
_srv_undo_state_last_call_ms[func_name] = -60000
var state_to_restore = _srv_undo_stack.pop_back()
_srv_undo_state_creation_disable()
rpc("set_state_compressed", compress_state(state_to_restore),
srv_get_next_piece_name())
_srv_undo_state_creation_enable()
# Let all players know if the undo stack is now empty.
if _srv_undo_stack.empty():
rpc("_on_undo_stack_empty")
# Pushes an undo state to the top of the undo stack if undo state creation has
# been enabled.
# func_name: The name of the function that was just called. This is used to
# track timeouts for certain functions. The function name needs to be in the
# UNDO_STATE_EVENT_TIMEOUTS_MS dictionary to be valid.
func push_undo_state(func_name: String) -> void:
if _srv_undo_disable_state_creation > 0:
return
if _table_body == null:
return
if _table_body.mode != RigidBody.MODE_STATIC:
return
if not UNDO_STATE_EVENT_TIMEOUTS_MS.has(func_name):
push_error("Function '%s' has no pre-set timeout!" % func_name)
return
var current_time_ms = OS.get_ticks_msec()
if _srv_undo_state_last_call_ms.has(func_name):
var last_time_ms: int = _srv_undo_state_last_call_ms[func_name]
if current_time_ms - last_time_ms < UNDO_STATE_EVENT_TIMEOUTS_MS[func_name]:
return
_srv_undo_state_last_call_ms[func_name] = current_time_ms
while _srv_undo_stack.size() >= UNDO_STACK_SIZE_LIMIT:
_srv_undo_stack.pop_front()
_srv_undo_stack.push_back(get_state(false, false))
# Let all players know that the undo stack has been pushed to.
rpc("_on_undo_stack_pushed")
# Remove a player's hand from the room.
# player: The ID of the player whose hand to remove.
remotesync func remove_hand(player: int) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var node_name = str(player)
if _hands.has_node(node_name):
var hand = _hands.get_node(node_name)
_hands.remove_child(hand)
hand.queue_free()
# Called by the server to remove a hidden area from the table.
# area_name: The name of the hidden area to remove.
remotesync func remove_hidden_area(area_name: String) -> void:
var hidden_area = _hidden_areas.get_node(area_name)
if not hidden_area:
push_error("Hidden area " + area_name + " does not exist!")
return
if not hidden_area is HiddenArea:
push_error("Node " + area_name + " is not a hidden area!")
return
if get_tree().is_network_server():
push_undo_state("remove_hidden_area")
_hidden_areas.remove_child(hidden_area)
hidden_area.queue_free()
# Called by the server to remove a piece from a container, a.k.a. having the
# piece be "released" by the container.
# container_name: The name of the container that is absorbing the piece.
# piece_name: The name of the piece that the container is releasing.
remotesync func remove_piece_from_container(container_name: String, piece_name: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var container = _pieces.get_node(container_name)
if not container:
push_error("Container " + container_name + " does not exist!")
return
if not container is PieceContainer:
push_error("Piece " + container_name + " is not a container!")
return
if not container.has_piece(piece_name):
push_error("Container " + container_name + " does not contain piece " + piece_name)
return
if get_tree().is_network_server():
push_undo_state("remove_piece_from_container")
# If there is a piece with the same name (in limbo), rename it so this piece
# can use it's own name.
if _pieces.has_node(piece_name):
var node = _pieces.get_node(piece_name)
var new_name = node.name + "_"
while _pieces.has_node(new_name):
new_name += "_"
node.name = new_name
var piece = container.remove_piece(piece_name)
_pieces.add_child(piece)
# Called by the server to remove pieces from the room.
# piece_names: The names of the pieces to remove.
remotesync func remove_pieces(piece_names: Array) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
for piece_name in piece_names:
if not piece_name is String:
push_error("Piece name is not a string!")
return
if not _pieces.has_node(piece_name):
push_error("Piece %s does not exist!" % piece_name)
return
var piece = _pieces.get_node(piece_name)
if not piece is Piece:
push_error("Object %s is not a piece!" % piece_name)
return
_camera_controller.remove_piece_ref(piece)
for player_id in _client_hover_pieces:
var hovering: Array = _client_hover_pieces[player_id]
hovering.erase(piece_name)
if get_tree().is_network_server():
push_undo_state("remove_pieces")
if Lobby.get_player_count() <= 1:
_pieces.remove_child(piece)
ResourceManager.queue_free_object(piece)
else:
# Clients put the piece in "limbo", until a certain amount of time
# has passed since the last RPC call.
if piece.is_in_group("limbo"):
continue
# Audio players will continue to play music until they have been
# removed from the scene tree, so stop music locally.
if piece is SpeakerPiece:
piece.stop_track()
piece.collision_layer = 0
piece.collision_mask = 0
piece.mode = RigidBody.MODE_STATIC
piece.visible = false
piece.set_process(false)
piece.set_physics_process(false)
piece.set_meta("entered_limbo", OS.get_ticks_msec())
piece.add_to_group("limbo")
# Request the server to add a piece to the game.
# Returns: The name of the new piece, an empty string if invalid.
# entry_path: The piece's entry path in the AssetDB.
# position: The position to spawn the piece at.
master func request_add_piece(entry_path: String, position: Vector3) -> String:
var transform = Transform(Basis.IDENTITY, position)
var piece_entry = AssetDB.search_path(entry_path)
if piece_entry.empty():
push_error("Invalid add request, entry not found: %s" % entry_path)
return ""
# Is the piece a pre-filled stack?
if piece_entry.has("entry_names"):
return request_add_stack_filled(transform, entry_path)
else:
# Adjust the initial position using information from the piece entry.
# NOTE: We don't need to do this for stacks, as add_stack_filled()
# does this for us.
var piece_height: float = 0.0
if piece_entry.has("bounding_box"):
var lower_corner: Vector3 = piece_entry["bounding_box"][0]
var upper_corner: Vector3 = piece_entry["bounding_box"][1]
piece_height = upper_corner.y - lower_corner.y
else:
var piece_scale: Vector3 = piece_entry["scale"]
piece_height = piece_scale.y
transform.origin.y += piece_height / 2.0
# Send the call to create the piece to everyone.
var piece_name = srv_get_next_piece_name()
rpc("add_piece", piece_name, transform, entry_path)
return piece_name
# Request the server to add a piece into a container.
# entry_path: The piece's entry path in the AssetDB.
# container_name: The name of the container to add the piece to.
master func request_add_piece_in_container(entry_path: String, container_name: String) -> void:
# Spawn the piece far away so the players don't see it.
var piece_name = request_add_piece(entry_path, Vector3(9999, 9999, 9999))
if piece_name.empty():
return
rpc("add_piece_to_container", container_name, piece_name)
# Request the server to add the given pieces to a container's contents.
# container_name: The name of the container to add the pieces to.
# piece_names: The names of the pieces to add to the container.
master func request_add_pieces_to_container(container_name: String, piece_names: Array) -> void:
for piece_name in piece_names:
if piece_name is String:
if piece_name != container_name:
rpc("add_piece_to_container", container_name, piece_name)
# Request the server to add cards to the given hand.
# card_names: The names of the cards to add to the hand. Note that the names
# of stacks are also allowed.
# hand_id: The player ID of the hand to add the cards to.
master func request_add_cards_to_hand(card_names: Array, hand_id: int) -> void:
var hand_name = str(hand_id)
if hand_id <= 0:
push_error("Hand ID " + hand_name + " is invalid!")
return
var hand = _hands.get_node(str(hand_id))
if not hand:
push_error("Hand " + hand_name + " does not exist!")
return
var cards = []
for card_name in card_names:
var piece = _pieces.get_node(card_name)
if not piece:
push_error("Piece " + card_name + " does not exist!")
continue
if not piece is Piece:
push_error("Object " + card_name + " is not a piece!")
continue
if piece is Card:
cards.append(piece)
elif piece is Stack:
var is_card = piece.is_card_stack()
if not is_card:
push_error("Stack " + card_name + " does not contain cards!")
continue
var new_card_names = []
var last_card_name = srv_get_next_piece_name()
for i in range(piece.get_piece_count() - 1):
var new_name = request_pop_stack(card_name, 1, false, i + 1.0,
last_card_name)
new_card_names.append(new_name)
new_card_names.append(last_card_name)
for name in new_card_names:
var card: Card = _pieces.get_node(name)
cards.append(card)
else:
push_error("Piece " + card_name + " is not a card or a stack!")
continue
for card in cards:
var success = hand.srv_add_card(card)
if not success:
push_error("Card " + card.name + " could not be hovered!")
# Request the server to add cards to the nearest hand. The hand is decided
# based on the card's hover offsets.
# card_names: The names of the cards to add to the hand. Note that the names
# of stacks of cards are also allowed.
master func request_add_cards_to_nearest_hand(card_names: Array) -> void:
var hand_id = 0
var min_dist = null
for card_name in card_names:
var piece = _pieces.get_node(card_name)
if not piece:
push_error("Piece " + card_name + " does not exist!")
continue
if not piece is Piece:
push_error("Object " + card_name + " is not a piece!")
continue
if piece.get("over_hands") == null:
push_error("Piece " + card_name + " does not have the over_hands property!")
continue
if not piece.over_hands.empty():
var piece_dist = piece.hover_offset.length()
if (min_dist == null) or (piece_dist < min_dist):
hand_id = piece.over_hands[0]
min_dist = piece_dist
if hand_id <= 0:
push_error("None of the cards were over a hand!")
return
request_add_cards_to_hand(card_names, hand_id)
# Request the server to add a pre-filled stack.
# Returns: The name of the new stack, an empty string if invalid.
# stack_transform: The transform the new stack should have.
# stack_entry_path: The stack's entry path in the AssetDB.
master func request_add_stack_filled(stack_transform: Transform,
stack_entry_path: String) -> String:
if AssetDB.search_path(stack_entry_path).empty():
push_error("Invalid add stack request, entry not found: %s" % stack_entry_path)
return ""
var stack_name = srv_get_next_piece_name()
rpc("add_stack_filled", stack_name, stack_transform, stack_entry_path)
return stack_name
# Request the server to collect a set of pieces and, if possible, put them into
# stacks.
# piece_names: The names of the pieces to try and collect.
master func request_collect_pieces(piece_names: Array) -> void:
var pieces = []
for piece_name in piece_names:
var piece = _pieces.get_node(piece_name)
if piece and piece is StackablePiece:
pieces.append(piece)
if pieces.size() <= 1:
return
push_undo_state("request_collect_pieces")
var add_to = pieces.pop_front()
while add_to:
for i in range(pieces.size() - 1, -1, -1):
var add_from = pieces[i]
if add_to.matches(add_from):
if add_to is Stack:
if add_from is Stack:
rpc("add_stack_to_stack", add_from.name, add_to.name,
add_from.transform, add_to.transform)
else:
rpc("add_piece_to_stack", add_from.name, add_to.name,
add_from.transform, add_to.transform)
else:
if add_from is Stack:
var single_height = add_to.get_size().y
var single_transform = add_to.transform
rpc("add_piece_to_stack", add_to.name, add_from.name,
add_to.transform, add_from.transform)
# add_to (Piece) has been added to add_from (Stack), so
# in future, we need to add pieces to add_from.
add_to = add_from
# Put the stack where the piece just was.
var height_adj = 0.5 * single_height * abs(single_transform.basis.y.y)
var new_origin = single_transform.origin + height_adj * Vector3.UP
var new_quat = single_transform.basis.get_rotation_quat()
add_to.request_set_translation(new_origin)
add_to.request_set_rotation_quat(new_quat)
else:
var new_stack_name = srv_get_next_piece_name()
rpc("add_stack", new_stack_name, add_from.name,
add_to.name, add_from.transform, add_to.transform)
add_to = _pieces.get_node(new_stack_name)
pieces.remove(i)
add_to = pieces.pop_front()
# Request the server to randomly release a set amount of pieces from a
# container.
# container_name: The name of the container to release pieces from.
# n: The number of pieces to release from the container.
# hover: Do we want to start hovering the piece afterwards?
master func request_container_release_random(container_name: String, n: int, hover: bool) -> void:
if n < 1:
return
var container = _pieces.get_node(container_name)
if not container:
push_error("Container " + container_name + " does not exist!")
return
if not container is PieceContainer:
push_error("Piece " + container_name + " is not a container!")
return
var names = container.get_piece_names()
if names.size() == 0:
return
# We want the selection to be random!
if n < names.size():
randomize()
names.shuffle()
names = names.slice(0, n - 1)
request_container_release_these(container_name, names, hover)
# Request the server to release a given set of pieces from a container.
# container_name: The name of the container to release the pieces from.
# release_names: The list of names of the pieces to be released from the
# container.
# hover: Do we want to start hovering the piece afterwards?
master func request_container_release_these(container_name: String,
release_names: Array, hover: bool) -> void:
if release_names.empty():
return
var player_id = get_tree().get_rpc_sender_id()
var container = _pieces.get_node(container_name)
if not container:
push_error("Container " + container_name + " does not exist!")
return
if not container is PieceContainer:
push_error("Piece " + container_name + " is not a container!")
return
var hover_name_arr = []
var hover_offet_arr = []
var init_pos = Vector3.ZERO
var is_init_pos_set = false
for piece_name in release_names:
if container.has_piece(piece_name):
rpc("remove_piece_from_container", container_name, piece_name)
if hover:
hover_name_arr.append(piece_name)
var calc_box_pos = Vector3.ZERO
var calc_box_size = Vector3.ZERO
var calc_direction = 0
var max_y_pos = 0
for piece_name in hover_name_arr:
var piece: Piece = _pieces.get_node(piece_name)
# TODO: This was already calculated by the container, ideally we should
# be able to use that value.
var piece_size = Global.rotate_bounding_box(piece.get_size(),
piece.transform.basis)
var piece_offset = calc_box_pos
if not is_init_pos_set:
init_pos = piece.transform.origin
is_init_pos_set = true
max_y_pos = max(max_y_pos, piece.transform.origin.y)
if calc_direction == 0:
piece_offset.x += (calc_box_size.x + piece_size.x) / 2
elif calc_direction == 1:
piece_offset.z += (calc_box_size.z + piece_size.z) / 2
elif calc_direction == 2:
piece_offset.x -= (calc_box_size.x + piece_size.x) / 2
else:
piece_offset.z -= (calc_box_size.z + piece_size.z) / 2
calc_box_pos = 0.5 * (calc_box_pos + piece_offset)
if calc_direction % 2 == 0:
calc_box_size.x += piece_size.x
calc_box_size.z = max(calc_box_size.z, piece_size.z)
else:
calc_box_size.x = max(calc_box_size.x, piece_size.x)
calc_box_size.z += piece_size.z
calc_direction = (calc_direction + 1) % 4
hover_offet_arr.append(piece_offset)
if not hover_name_arr.empty():
_camera_controller.rpc_id(player_id, "set_hover_height", max_y_pos)
_srv_undo_state_creation_disable()
if hover_name_arr.size() == 1:
request_hover_piece(hover_name_arr[0], init_pos, hover_offet_arr[0])
else:
request_hover_pieces(hover_name_arr, init_pos, hover_offet_arr)
_srv_undo_state_creation_enable()
# Request the server to deal cards from a stack to all players.
# stack_name: The name of the stack of cards.
# n: The number of cards to deal to each player.
master func request_deal_cards(stack_name: String, n: int) -> void:
if n < 1:
return
var stack = _pieces.get_node(stack_name)
if not stack:
push_error("Piece " + stack_name + " does not exist!")
return
if not stack is Stack:
push_error("Piece " + stack_name + " is not a stack!")
return
var is_card_stack = stack.is_card_stack()
if not is_card_stack:
push_error("Stack " + stack_name + " does not contain cards!")
return
var last_name = ""
for _i in range(n):
for hand in _hands.get_children():
if not last_name.empty():
request_add_cards_to_hand([last_name], hand.owner_id())
break
elif not stack.empty():
if stack.get_piece_count() == 2:
last_name = srv_get_next_piece_name()
var card_name = request_pop_stack(stack_name, 1, false, 1.0, last_name)
request_add_cards_to_hand([card_name], hand.owner_id())
else:
break
# Request the server to flip the table.
# camera_basis: The basis matrix of the player flipping the table.
master func request_flip_table(camera_basis: Basis) -> void:
rpc("flip_table", camera_basis)
# Request the server to hover a piece.
# Returns: If the request was successful.
# piece_name: The name of the piece to hover.
# init_pos: The initial hover position.
# offset_pos: The hover position offset.
master func request_hover_piece(piece_name: String, init_pos: Vector3,
offset_pos: Vector3) -> bool:
var piece = _pieces.get_node(piece_name)
if not piece:
push_error("Piece " + piece_name + " does not exist!")
return false
if not piece is Piece:
push_error("Object " + piece_name + " is not a piece!")
return false
var player_id = get_tree().get_rpc_sender_id()
if piece.srv_start_hovering(player_id, init_pos, offset_pos):
push_undo_state("request_hover_piece")
rpc_id(player_id, "request_hover_piece_accepted", piece_name)
return true
return false
# Request the server to hover a set of pieces. The server will remember the
# list of pieces given, and use it to re-set the hover position of those pieces
# if the client wishes to update it.
# piece_names: The names of the pieces to hover.
# init_pos: The initial hover position.
# offset_pos_arr: The hover position offsets for each piece.
master func request_hover_pieces(piece_names: Array, init_pos: Vector3,
offset_pos_arr: Array) -> void:
if piece_names.size() != offset_pos_arr.size():
push_error("Name and offset arrays differ in size (name = %d, offset = %d)!" % [piece_names.size(), offset_pos_arr.size()])
return
var player_id = get_tree().get_rpc_sender_id()
_client_hover_pieces[player_id] = []
for i in range(len(piece_names)):
var piece_name: String = piece_names[i]
var offset_pos: Vector3 = offset_pos_arr[i]
if request_hover_piece(piece_name, init_pos, offset_pos):
_client_hover_pieces[player_id].append(piece_name)
# Send the hover pieces to the other clients so that they can hover the
# pieces when the hovering player sends an update.
rpc("set_client_hover_pieces", player_id, piece_names)
# Called by the server if the request to hover a piece was accepted.
# piece_name: The name of the piece we are now hovering.
remotesync func request_hover_piece_accepted(piece_name: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var piece = _pieces.get_node(piece_name)
if not piece:
push_error("Piece " + piece_name + " does not exist!")
return
if not piece is Piece:
push_error("Object " + piece_name + " is not a piece!")
return
_camera_controller.append_selected_pieces([piece])
_camera_controller.set_is_hovering(true)
# Request the server to load a compressed table state.
# compressed_state: The compressed state to load.
master func request_load_table_state(compressed_state: Dictionary) -> void:
rpc("set_state_compressed", compressed_state, srv_get_next_piece_name())
# Request the server to paste the contents of a clipboard to the room.
# clipboard: The clipboard contents (from _append_piece_states).
# offset: Offset the positions of the pasted pieces by this value.
master func request_paste_clipboard(clipboard: Dictionary, offset: Vector3) -> void:
_modify_piece_states(clipboard, offset)
rpc("paste_clipboard", clipboard, srv_get_next_piece_name())
# Request the server to place a hidden area registered to you.
# point1: One corner of the new hidden area.
# point2: The opposite corner of the new hidden area.
master func request_place_hidden_area(point1: Vector2, point2: Vector2) -> void:
var size = (point2 - point1).abs()
if size.x < HIDDEN_AREA_MIN_SIZE.x or size.y < HIDDEN_AREA_MIN_SIZE.y:
return
var player_id = get_tree().get_rpc_sender_id()
rpc("place_hidden_area", srv_get_next_piece_name(), player_id, point1, point2)
# Request the server to pop the piece at the top of a stack.
# Returns: The name of the new piece.
# stack_name: The name of the stack to pop.
# n: The number of pieces to pop from the stack.
# hover: Do we want to start hovering the piece afterwards?
# split_dist: How far away do we want the piece from the stack when it is poped?
# last_name: If there is only one piece left in the stack, it is optionally
# given this name.
master func request_pop_stack(stack_name: String, n: int, hover: bool,
split_dist: float, last_name: String = "") -> String:
var player_id = get_tree().get_rpc_sender_id()
var stack = _pieces.get_node(stack_name)
if not stack:
push_error("Stack " + stack_name + " does not exist!")
return ""
if not stack is Stack:
push_error("Object " + stack_name + " is not a stack!")
return ""
var new_piece: Piece = null
if n < 1:
return ""
elif n < stack.get_piece_count():
push_undo_state("request_pop_stack")
# Since there is a lot of piece manipulation in this function, disable
# creating undo states mid-way through.
_srv_undo_state_creation_disable()
var unit_height = stack.get_unit_height()
var total_height = stack.get_total_height()
var removed_height = unit_height * n
# NOTE: We normalise the basis here to reset the piece's scale, because
# add_piece will use the piece entry to scale the piece again.
var new_basis = stack.transform.basis.orthonormalized()
var new_origin = stack.transform.origin
new_origin.y += total_height / 2
# Get the new piece away from the stack so it doesn't collide with it
# again.
new_origin.y += split_dist + removed_height / 2
var new_name = srv_get_next_piece_name()
if n == 1:
var index = stack.pop_index()
var piece_meta = stack.remove_piece(index)
stack.rpc("remove_piece", index)
var piece_entry = piece_meta["piece_entry"]
var piece_transform = piece_meta["transform"]
new_basis = (stack.transform.basis * piece_transform.basis).orthonormalized()
rpc("add_piece", new_name, Transform(new_basis, new_origin),
piece_entry["entry_path"])
new_piece = _pieces.get_node(new_name)
else:
var new_transform = Transform(new_basis, new_origin)
new_piece = add_stack_empty(new_name, new_transform, stack is StackSandwich)
rpc("add_stack_empty", new_name, new_transform, stack is StackSandwich)
rpc("transfer_stack_contents", stack_name, new_name, stack.transform, n)
# Move the stack down to it's new location.
var new_stack_translation = stack.translation
var offset = stack.transform.basis.y.normalized()
if offset.y > 0:
offset = -offset
new_stack_translation += offset * (removed_height / 2)
stack.rpc("set_translation", new_stack_translation)
# If there is only one piece left in the stack, turn it into a normal
# piece.
if stack.get_piece_count() == 1:
if last_name.empty():
last_name = srv_get_next_piece_name()
var piece_meta = stack.remove_piece(0)
var piece_entry = piece_meta["piece_entry"]
var piece_transform = piece_meta["transform"]
rpc("remove_pieces", [stack_name])
new_basis = (stack.transform.basis * piece_transform.basis).orthonormalized()
rpc("add_piece", last_name,
Transform(new_basis, new_stack_translation),
piece_entry["entry_path"])
_srv_undo_state_creation_enable()
else:
new_piece = stack
if new_piece and hover:
if new_piece.srv_start_hovering(player_id, new_piece.transform.origin, Vector3()):
rpc_id(player_id, "request_pop_stack_accepted", new_piece.name)
return new_piece.name
# Called by the server if the request to pop a stack was accepted, and we are
# now hovering the new piece.
# piece_name: The name of the piece that is now hovering.
remotesync func request_pop_stack_accepted(piece_name: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
# The server has allowed us to hover the piece that has just poped off the
# stack!
request_hover_piece_accepted(piece_name)
# Request the server to remove a hidden area.
# area_name: The name of the hidden area to remove.
master func request_remove_hidden_area(area_name: String) -> void:
var hidden_area = _hidden_areas.get_node(area_name)
if not hidden_area:
push_error("Hidden area " + area_name + " does not exist!")
return
if not hidden_area is HiddenArea:
push_error("Node " + area_name + " is not a hidden area!")
return
rpc("remove_hidden_area", area_name)
# Request the server to remove a set of pieces from the table.
# piece_names: The names of the pieces to remove.
master func request_remove_pieces(piece_names: Array) -> void:
for piece_name in piece_names:
if not piece_name is String:
push_error("Piece name in array is not a string!")
return
if not _pieces.has_node(piece_name):
push_error("Piece with name %s does not exist!" % piece_name)
return
var piece = _pieces.get_node(piece_name)
if not piece is Piece:
push_error("Object %s is not a piece!" % piece_name)
return
rpc("remove_pieces", piece_names)
# Request the server to set the lamp color.
# color: The color to set the lamp to.
master func request_set_lamp_color(color: Color) -> void:
rpc("set_lamp_color", color)
# Request the server to set the lamp intensity.
# intensity: The intensity to set the lamp to.
master func request_set_lamp_intensity(intensity: float) -> void:
rpc("set_lamp_intensity", intensity)
# Request the server to set the lamp type.
# sunlight: True for sunlight, false for a spotlight.
master func request_set_lamp_type(sunlight: bool) -> void:
rpc("set_lamp_type", sunlight)
# Request the server to set the room skybox.
# skybox_entry_path: The skybox's entry path in the asset DB.
master func request_set_skybox(skybox_entry_path: String) -> void:
if AssetDB.search_path(skybox_entry_path).empty():
push_error("Invalid set skybox request, entry not found: %s" % skybox_entry_path)
rpc("set_skybox", skybox_entry_path)
# Request the server to set the room table.
# table_entry_path: The table's entry path in the asset DB.
master func request_set_table(table_entry_path: String) -> void:
if AssetDB.search_path(table_entry_path).empty():
push_error("Invalid set table request, entry not found: %s" % table_entry_path)
rpc("set_table", table_entry_path)
# Request the server to spawn the fast circle.
master func request_spawn_fast_circle() -> void:
rpc("spawn_fast_circle")
# Request the server to get a stack to collect all of the pieces that it can
# stack.
# stack_name: The name of the collecting stack.
# collect_stacks: Do we want to collect other stacks? If false, it only collects
# individual pieces.
master func request_stack_collect_all(stack_name: String, collect_stacks: bool) -> void:
var stack = _pieces.get_node(stack_name)
if not stack:
push_error("Stack " + stack_name + " does not exist!")
return
if not stack is Stack:
push_error("Object " + stack_name + " is not a stack!")
return
push_undo_state("request_stack_collect_all")
for piece in get_pieces():
if piece is StackablePiece and piece.name != stack_name:
if stack.matches(piece):
if piece is Stack:
if collect_stacks:
rpc("add_stack_to_stack", piece.name, stack_name,
piece.transform, stack.transform)
else:
continue
else:
rpc("add_piece_to_stack", piece.name, stack_name,
piece.transform, stack.transform, Stack.STACK_TOP)
# Called by the server to set a client's hovering pieces.
remote func set_client_hover_pieces(player_id: int, piece_names: Array) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
_client_hover_pieces[player_id] = piece_names
# Request the server to set the hover position of multiple pieces - note that
# the pieces that are updated are defined when calling request_hover_pieces.
# hover_position: The new hover position.
master func set_hover_position_multiple(hover_position: Vector3) -> void:
rpc_unreliable("set_hover_position_multiple_client",
get_tree().get_rpc_sender_id(), hover_position)
# Called by the server to set the hover position of multiple pieces.
# player_id: The ID of the player updating the hover positions.
# hover_position: The new hover position.
remotesync func set_hover_position_multiple_client(player_id: int,
hover_position: Vector3) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var piece_names = _client_hover_pieces[player_id]
for piece_name in piece_names:
var piece = _pieces.get_node(piece_name)
if not piece:
push_error("Piece " + piece_name + " does not exist!")
return
if not piece is Piece:
push_error("Object " + piece_name + " is not a piece!")
return
# The client_set_hover_position signal is only fired during the request
# on the server side for single-piece hovering - so for multi-piece
# hovering, we'll need to fire the signal ourselves, since the room
# directly calls Piece.set_hover_position.
if get_tree().is_network_server():
piece.emit_signal("client_set_hover_position", piece)
piece.set_hover_position(hover_position)
# Set the color of the room lamp.
# color: The color of the lamp.
remotesync func set_lamp_color(color: Color) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
# NOTE: This is usually called alongside set_table(), so push an undo state
# under that name so we don't create multiple states.
if get_tree().has_network_peer() and get_tree().is_network_server():
push_undo_state("set_table")
_spot_light.light_color = color
_sun_light.light_color = color
# Set the intensity of the room lamp.
# intensity: The new intensity of the lamp.
remotesync func set_lamp_intensity(intensity: float) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
# NOTE: This is usually called alongside set_table(), so push an undo state
# under that name so we don't create multiple states.
if get_tree().has_network_peer() and get_tree().is_network_server():
push_undo_state("set_table")
_spot_light.light_energy = intensity
_sun_light.light_energy = intensity
# Set the type of light the room lamp is emitting.
# sunlight: True for sunlight, false for a spotlight.
remotesync func set_lamp_type(sunlight: bool) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
# NOTE: This is usually called alongside set_table(), so push an undo state
# under that name so we don't create multiple states.
if get_tree().has_network_peer() and get_tree().is_network_server():
push_undo_state("set_table")
_spot_light.visible = not sunlight
_sun_light.visible = sunlight
# Set the room's skybox.
# skybox_entry_path: The skybox's entry path in the asset DB. If either the
# texture path or the entry are empty, the default skybox is used.
remotesync func set_skybox(skybox_entry_path: String) -> void:
if get_tree().has_network_peer():
if get_tree().get_rpc_sender_id() != 1:
return
var skybox_entry = AssetDB.search_path(skybox_entry_path)
if skybox_entry.empty():
push_error("Cannot set skybox, entry not found: %s" % skybox_entry_path)
return
# Changing the skybox can take a long time if the radiance size is big, so
# avoid doing it if the skybox being set is the same as the current skybox.
if _world_environment.has_meta("skybox_entry"):
var current_entry = _world_environment.get_meta("skybox_entry")
if current_entry.hash() == skybox_entry.hash():
return
# NOTE: This is usually called alongside set_table(), so push an undo state
# under that name so we don't create multiple states.
if get_tree().has_network_peer() and get_tree().is_network_server():
push_undo_state("set_table")
# Free the current skybox before we create a new one, just so we don't use
# as much memory as we need to.
var radiance = _world_environment.environment.background_sky.radiance_size
_world_environment.environment.background_sky = null
var skybox: Sky = null
if not skybox_entry.empty():
if skybox_entry.has("texture_path"):
var texture_path = skybox_entry["texture_path"]
if not texture_path.empty():
var texture: Texture = ResourceManager.load_res(texture_path)
skybox = PanoramaSky.new()
skybox.panorama = texture
if skybox == null:
skybox = ProceduralSky.new()
skybox.radiance_size = radiance
_world_environment.environment.background_sky = skybox
_world_environment.environment.background_sky_rotation_degrees = skybox_entry["rotation"]
_world_environment.environment.background_energy = skybox_entry["strength"]
_world_environment.set_meta("skybox_entry", skybox_entry)
# Set the room state.
# state: The new room state.
# first_name: The new name for the first piece in the state. If empty, the
# saved name is used instead.
func set_state(state: Dictionary, first_name: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
if not first_name.empty():
# _extract_piece_states() will use srv_get_next_piece_name() for all
# clients to determine the new piece names.
if not first_name.is_valid_integer():
push_error("First name %s is not a valid integer!" % first_name)
return
_srv_next_piece_name = int(first_name)
if get_tree().is_network_server():
push_undo_state("set_state")
_srv_undo_state_creation_disable()
if state.has("lamp"):
var lamp_meta = state["lamp"]
set_lamp_color(lamp_meta["color"])
set_lamp_intensity(lamp_meta["intensity"])
set_lamp_type(lamp_meta["sunlight"])
if state.has("skybox"):
set_skybox(state["skybox"])
if state.has("table"):
var table_meta = state["table"]
set_table(table_meta["entry_path"])
if _table_body:
if table_meta["is_rigid"]:
_table_body.mode = RigidBody.MODE_RIGID
else:
_table_body.mode = RigidBody.MODE_STATIC
_table_body.transform = table_meta["transform"]
if table_meta["is_rigid"]:
emit_signal("table_flipped")
else:
emit_signal("table_unflipped")
if get_tree().is_network_server():
srv_set_retrieve_pieces_from_hell(not table_meta["is_rigid"])
var paint_image_data = table_meta["paint_image_data"]
if paint_image_data == null:
_paint_plane.clear_paint()
else:
var paint_image = Image.new()
var paint_image_size = _paint_plane.get_paint_size()
paint_image.create_from_data(paint_image_size.x, paint_image_size.y,
false, _paint_plane.PAINT_FORMAT, paint_image_data)
_paint_plane.set_paint(paint_image)
if state.has("hands"):
for hand in _hands.get_children():
_hands.remove_child(hand)
hand.queue_free()
for hand_id in state["hands"]:
var hand_name = str(hand_id)
var hand_meta = state["hands"][hand_id]
if not hand_meta.has("transform"):
push_error("Hand " + hand_name + " in new state has no transform!")
return
if not hand_meta["transform"] is Transform:
push_error("Hand " + hand_name + " transform is not a transform!")
return
add_hand(hand_id, hand_meta["transform"])
if state.has("hidden_areas"):
for hidden_area in _hidden_areas.get_children():
_hidden_areas.remove_child(hidden_area)
hidden_area.queue_free()
for hidden_area_name in state["hidden_areas"]:
# Make sure the server doesn't duplicate names! We need to do this
# because hidden areas use the same naming system as pieces do.
if get_tree().is_network_server():
var name_int = int(hidden_area_name)
if name_int >= _srv_next_piece_name:
_srv_next_piece_name = name_int + 1
var hidden_area_meta = state["hidden_areas"][hidden_area_name]
if not hidden_area_meta.has("player_id"):
push_error("Hidden area " + hidden_area_name + " in new state has no player ID!")
return
if not hidden_area_meta["player_id"] is int:
push_error("Hidden area " + hidden_area_name + " player ID is not an integer!")
return
if not hidden_area_meta.has("point1"):
push_error("Hidden area " + hidden_area_name + " in new state has no point 1!")
return
if not hidden_area_meta["point1"] is Vector2:
push_error("Hidden area" + hidden_area_name + " point 1 is not a Vector2!")
return
if not hidden_area_meta.has("point2"):
push_error("Hidden area " + hidden_area_name + " in new state has no point 2!")
return
if not hidden_area_meta["point2"] is Vector2:
push_error("Hidden area" + hidden_area_name + " point 2 is not a Vector2!")
return
var player_id = hidden_area_meta["player_id"]
var point1 = hidden_area_meta["point1"]
var point2 = hidden_area_meta["point2"]
if Lobby.player_exists(player_id):
place_hidden_area(hidden_area_name, player_id, point1, point2)
if first_name.empty():
# If we are using the original names, we need to forcefully remove all
# existing pieces, since they may use the same name.
for child in _pieces.get_children():
_pieces.remove_child(child)
ResourceManager.queue_free_object(child)
_client_hover_pieces.clear()
else:
# If the names will be replaced, then we can safely put pieces into
# limbo, since the new names should not clash.
var old_piece_name_arr = []
for child in _pieces.get_children():
old_piece_name_arr.append(child.name)
remove_pieces(old_piece_name_arr)
_extract_piece_states(state, _pieces, not first_name.empty())
# Wait a few physics frames for the hands to detect the cards, then add the
# cards to the hands.
if get_tree().is_network_server():
_srv_allow_card_stacking = false
_srv_hand_setup_frames = 5
_srv_undo_state_creation_enable()
# Set the room state with a compressed version of a state.
# compressed_state: The compressed state from get_state_compressed().
# first_name: The new name for the first piece in the state. If empty, the
# saved name is used instead.
remotesync func set_state_compressed(compressed_state: Dictionary, first_name: String) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
if not compressed_state.has("data"):
push_error("Compressed state does not contain data!")
return
if not compressed_state["data"] is PoolByteArray:
push_error("Compressed state data is not a byte array!")
return
if not compressed_state.has("size"):
push_error("Compressed state does not have size information!")
return
if not compressed_state["size"] is int:
push_error("Compressed state size information is not an integer!")
return
var data = compressed_state["data"]
var size = compressed_state["size"]
var bytes = data.decompress(size, File.COMPRESSION_FASTLZ)
var state = bytes2var(bytes)
if state is Dictionary:
set_state(state, first_name)
else:
push_error("Failed to decode the uncompressed state!")
# Set the room table.
# table_entry_path: The table's entry path in the asset DB.
remotesync func set_table(table_entry_path: String) -> void:
if get_tree().has_network_peer():
if get_tree().get_rpc_sender_id() != 1:
return
var table_entry = AssetDB.search_path(table_entry_path)
if table_entry.empty():
push_error("Cannot set table, entry not found: %s" % table_entry_path)
return
if _table_body != null:
# Changing the table can take a while if the model is very detailed,
# so avoid doing it if the table being set is the same as the one
# already in the room.
if _table_body.has_meta("table_entry"):
var current_entry = _table_body.get_meta("table_entry")
if current_entry.hash() == table_entry.hash():
return
if get_tree().has_network_peer() and get_tree().is_network_server():
push_undo_state("set_table")
if _table_body.is_a_parent_of(_paint_plane):
_table_body.remove_child(_paint_plane)
_table.remove_child(_table_body)
ResourceManager.queue_free_object(_table_body)
_table_body = null
for hand_pos in _hand_positions.get_children():
_hand_positions.remove_child(hand_pos)
hand_pos.queue_free()
if not table_entry.empty():
if table_entry.has("bounding_box"):
var bounding_box: Array = table_entry["bounding_box"]
var size: Vector3 = bounding_box[1] - bounding_box[0]
var side_length = 3.0 * max(size.x, size.z)
_fast_circle.scale = Vector3(side_length, 1.0, side_length)
if table_entry.has("scene_path"):
if not table_entry["scene_path"].empty():
if PieceCache.should_cache(table_entry):
var table_cache = PieceCache.new(table_entry_path, false)
var maybe_table = table_cache.get_scene()
if maybe_table != null and not maybe_table is Piece:
_table_body = maybe_table
else:
if maybe_table != null:
ResourceManager.free_object(maybe_table)
_table_body = PieceBuilder.build_table(table_entry)
else:
_table_body = PieceBuilder.build_table(table_entry)
_table_body.name = "TableBody"
_table.add_child(_table_body)
for hand_meta in table_entry["hands"]:
var pos: Vector3 = hand_meta["pos"]
var dir: float = deg2rad(hand_meta["dir"])
var hand = Spatial.new()
hand.transform = hand.transform.rotated(Vector3.UP, dir)
hand.transform.origin = pos
_hand_positions.add_child(hand)
if get_tree().has_network_peer():
if get_tree().is_network_server():
srv_update_hand_transforms()
if _table_body != null:
_table_body.add_child(_paint_plane)
_paint_plane.global_transform.origin = Vector3(0.0, 0.001, 0.0)
# The table, being a RigidBody, should not have it's own scale.
var paint_plane_size = table_entry["paint_plane"]
_paint_plane.scale = Vector3(paint_plane_size.x, 1.0, paint_plane_size.y)
_paint_plane.clear_paint()
# Spawn the fast circle.
remotesync func spawn_fast_circle() -> void:
if get_tree().get_rpc_sender_id() != 1:
return
if not _fast_circle.is_processing():
_fast_circle.restart()
# Get the next hand transform. Note that there may not be a next transform, in
# which case the function returns the identity transform.
# Returns: The next hand transform.
func srv_get_next_hand_transform() -> Transform:
var potential = []
for position in _hand_positions.get_children():
potential.append(position.transform)
for hand in _hands.get_children():
if potential.has(hand.transform):
potential.erase(hand.transform)
if potential.empty():
return Transform.IDENTITY
else:
return potential[0]
# Get the next piece name.
# Returns: The next piece name.
func srv_get_next_piece_name() -> String:
var next_name = str(_srv_next_piece_name)
_srv_next_piece_name += 1
return next_name
# Set whether the server should retrieve pieces from hell.
# retrieve: If the server should retrieve pieces from hell.
func srv_set_retrieve_pieces_from_hell(retrieve: bool) -> void:
_srv_retrieve_pieces_from_hell = retrieve
for piece in _pieces.get_children():
if piece is Piece:
piece.srv_retrieve_from_hell = retrieve
# Stop a player from currently hovering any pieces.
# player: The player to stop from hovering.
func srv_stop_player_hovering(player: int) -> void:
for piece in _pieces.get_children():
if piece.hover_player == player:
piece.rpc_id(1, "request_stop_hovering")
# Update the transforms of the hands to match the table entry's hand positions.
func srv_update_hand_transforms() -> void:
var hand_index = 0
for hand in _hands.get_children():
var hand_player = int(hand.name)
if hand_index < _hand_positions.get_child_count():
var hand_transform = _hand_positions.get_child(hand_index).transform
hand.rpc("update_transform", hand_transform)
else:
rpc("remove_hand", hand_player)
hand_index += 1
var player_ids = Lobby.get_player_list()
var next_transform = srv_get_next_hand_transform()
while (not player_ids.empty()) and next_transform != Transform.IDENTITY:
var player_id = player_ids.pop_front()
if not _hands.has_node(str(player_id)):
rpc("add_hand", player_id, next_transform)
next_transform = srv_get_next_hand_transform()
# Start sending the player's 3D cursor position to the server.
func start_sending_cursor_position() -> void:
_camera_controller.send_cursor_position = true
# Transfer the contents at the top of one stack to the top of another.
# stack1_name: The name of the stack to transfer contents from.
# stack2_name: The name of the stack to transfer contents to.
# stack1_transform: The server's transform for the first stack.
# n: The number of contents to transfer.
remotesync func transfer_stack_contents(stack1_name: String, stack2_name: String,
stack1_transform: Transform, n: int) -> void:
if get_tree().get_rpc_sender_id() != 1:
return
var stack1 = _pieces.get_node(stack1_name)
var stack2 = _pieces.get_node(stack2_name)
if not stack1:
push_error("Stack " + stack1_name + " does not exist!")
return
if not stack2:
push_error("Stack " + stack2_name + " does not exist!")
return
if not stack1 is Stack:
push_error("Piece " + stack1_name + " is not a stack!")
return
if not stack2 is Stack:
push_error("Piece " + stack2_name + " is not a stack!")
return
n = int(min(n, stack1.get_piece_count()))
if n < 1:
return
stack1.transform = stack1_transform
var contents = []
for _i in range(n):
var index = stack1.pop_index()
contents.push_back(stack1.remove_piece(index))
var stack_facing_up = stack1.transform.basis.y.y > 0.0
var add_to = Stack.STACK_TOP if stack_facing_up else Stack.STACK_BOTTOM
while not contents.empty():
var piece_meta = contents.pop_back()
var piece_entry = piece_meta["piece_entry"]
var piece_transform = piece_meta["transform"]
piece_transform.basis = stack1.transform.basis * piece_transform.basis
stack2.add_piece(piece_entry, piece_transform, add_to)
func _ready():
_srv_undo_state_creation_disable()
Lobby.connect("player_added", self, "_on_Lobby_player_added")
var skybox = AssetDB.random_asset("TabletopClub", "skyboxes", true)
if not skybox.empty():
# TODO: Consider setting this directly?
set_skybox(skybox["entry_path"])
var table = AssetDB.random_asset("TabletopClub", "tables", true)
if not table.empty():
set_table(table["entry_path"])
_fast_circle.set_process(false)
_srv_undo_state_creation_enable()
func _physics_process(_delta):
if not get_tree().has_network_peer():
return
if get_tree().is_network_server():
# TODO: This does not need to be done every frame, find a way to run
# this just whenever the number of active pieces changes.
_srv_update_bandwidth_throttle()
if _srv_hand_setup_frames >= 0:
_srv_hand_setup_frames -= 1
if _srv_hand_setup_frames == 0:
for piece in _pieces.get_children():
if piece is Card:
if piece.over_hands.size() == 1:
var hand_name = str(piece.over_hands[0])
if _hands.has_node(hand_name):
var hand = _hands.get_node(hand_name)
var ok = hand.srv_add_card(piece)
if not ok:
push_error("Failed to add card %s to the hand of player %s!" %
[piece.name, hand_name])
_srv_allow_card_stacking = true
# Append the states of pieces to a given dictionary.
# state: The dictionary to add the states to.
# pieces: The list of pieces to scan from.
# collisions: Should collision data be included in the state?
func _append_piece_states(state: Dictionary, pieces: Array, collisions: bool) -> void:
state["containers"] = {}
state["pieces"] = {}
state["speakers"] = {}
state["stacks"] = {}
state["timers"] = {}
for piece in pieces:
if piece.is_in_group("limbo"):
continue
var piece_meta = {
"entry_path": piece.piece_entry["entry_path"],
"is_locked": piece.is_locked(),
"transform": piece.transform,
"user_scale": piece.get_current_scale()
}
if piece.is_albedo_color_exposed():
var color = piece.get_albedo_color()
if piece.piece_entry["color"] != color:
piece_meta["color"] = color
if piece is PieceContainer:
var child_pieces = {}
if piece.has_node("Pieces"):
var children = piece.get_node("Pieces").get_children()
_append_piece_states(child_pieces, children, collisions)
piece_meta["pieces"] = child_pieces
state["containers"][piece.name] = piece_meta
elif piece is Stack:
# If the piece is a stack, we don't need to store the stack's piece
# entry, as it will figure it out itself once the first piece is
# added.
piece_meta.erase("entry_path")
var child_pieces = []
for child_piece in piece.get_pieces():
var child_piece_meta = {
"flip_y": child_piece["flip_y"],
"entry_path": child_piece["piece_entry"]["entry_path"]
}
child_pieces.push_back(child_piece_meta)
piece_meta["pieces"] = child_pieces
state["stacks"][piece.name] = piece_meta
elif piece is SpeakerPiece or piece is TimerPiece:
piece_meta["is_music_track"] = piece.is_music_track()
piece_meta["is_playing"] = piece.is_playing_track()
piece_meta["is_positional"] = piece.is_positional()
piece_meta["is_track_paused"] = piece.is_track_paused()
piece_meta["playback_position"] = piece.get_playback_position()
piece_meta["track_entry"] = piece.get_track()
piece_meta["unit_size"] = piece.get_unit_size()
if piece is TimerPiece:
piece_meta["is_timer_paused"] = piece.is_timer_paused()
piece_meta["mode"] = piece.get_mode()
piece_meta["time"] = piece.get_time()
state["timers"][piece.name] = piece_meta
else:
state["speakers"][piece.name] = piece_meta
else:
if collisions:
if piece is Card:
piece_meta["is_collisions_on"] = piece.is_collisions_on()
state["pieces"][piece.name] = piece_meta
# Extract the pieces from a room state, and add them to the scene tree.
# state: The state to extract the pieces from.
# parent: The node to add the pieces to as children.
# new_names: Should the extracted pieces use completely new names?
func _extract_piece_states(state: Dictionary, parent: Node, new_names: bool) -> void:
_extract_piece_states_type(state, parent, new_names, "containers")
_extract_piece_states_type(state, parent, new_names, "pieces")
_extract_piece_states_type(state, parent, new_names, "speakers")
_extract_piece_states_type(state, parent, new_names, "stacks")
_extract_piece_states_type(state, parent, new_names, "timers")
# A helper function when extracting piece states from a room state.
# state: The state to extract the pieces from.
# parent: The node to add the pieces to as children.
# new_names: Should the extracted pieces use completely new names?
# type_key: The key to extract from the state.
func _extract_piece_states_type(state: Dictionary, parent: Node, new_names: bool,
type_key: String) -> void:
if not state.has(type_key):
return
for old_piece_name in state[type_key]:
var piece_meta = state[type_key][old_piece_name]
# Make sure the server doesn't duplicate piece names!
if not new_names and get_tree().is_network_server():
var name_int = int(old_piece_name)
if name_int >= _srv_next_piece_name:
_srv_next_piece_name = name_int + 1
if not piece_meta.has("is_locked"):
push_error("Piece " + type_key + "/" + old_piece_name + " in new state has no is locked value!")
return
if not piece_meta["is_locked"] is bool:
push_error("Piece " + type_key + "/" + old_piece_name + " is locked value is not a boolean!")
return
if not piece_meta.has("transform"):
push_error("Piece " + type_key + "/" + old_piece_name + " in new state has no transform!")
return
if not piece_meta["transform"] is Transform:
push_error("Piece " + type_key + "/" + old_piece_name + " transform is not a transform!")
return
# Stacks don't include their piece entry, since they can figure it out
# themselves once the first piece is added.
if type_key != "stacks":
if not piece_meta.has("entry_path"):
push_error("Piece " + type_key + "/" + old_piece_name + " in new state has no entry path!")
return
if not piece_meta["entry_path"] is String:
push_error("Piece " + type_key + "/" + old_piece_name + " entry path is not a string!")
return
var new_piece_name = old_piece_name
if new_names:
# All clients will use this function (even though it starts with
# srv_) to make sure they generate the same names as the server.
new_piece_name = srv_get_next_piece_name()
# This should never be the case, but just in case!
if parent.has_node(new_piece_name):
push_warning("Piece %s already exists in parent %s, yeeting." %
[new_piece_name, parent.name])
var sus_imposter = parent.get_node(new_piece_name)
parent.remove_child(sus_imposter)
ResourceManager.queue_free_object(sus_imposter)
if type_key == "stacks":
var pieces: Array = piece_meta["pieces"]
if pieces.empty():
push_error("Piece " + type_key + "/" + old_piece_name + " has an empty 'pieces' array!")
return
var element_meta: Dictionary = pieces[0]
if not element_meta.has("entry_path"):
push_error("Piece " + type_key + "/" + old_piece_name + " element meta has no entry path!")
return
var entry_path: String = element_meta["entry_path"]
var piece_entry = AssetDB.search_path(entry_path)
if not piece_entry.empty():
var sandwich_stack = (piece_entry["scene_path"] == "res://Pieces/Card.tscn")
add_stack_empty(new_piece_name, piece_meta["transform"], sandwich_stack)
else:
push_error("Cannot add stack, first entry not found: %s" % entry_path)
else:
add_piece(new_piece_name, piece_meta["transform"], piece_meta["entry_path"])
var piece: Piece = _pieces.get_node(new_piece_name)
if piece_meta["is_locked"]:
piece.lock_client(piece_meta["transform"])
if piece_meta.has("user_scale"):
if not piece_meta["user_scale"] is Vector3:
push_error("Piece " + type_key + "/" + old_piece_name + " user scale is not a Vector3!")
return
piece.set_current_scale(piece_meta["user_scale"])
if piece_meta.has("color"):
if not piece_meta["color"] is Color:
push_error("Piece " + type_key + "/" + old_piece_name + " color is not a color!")
return
var color = piece_meta["color"]
if piece.is_albedo_color_exposed():
piece.set_albedo_color_client(color)
if type_key == "containers":
if not piece_meta.has("pieces"):
push_error("Container piece does not have a pieces entry!")
return
if not piece_meta["pieces"] is Dictionary:
push_error("Container pieces entry is not a dictionary!")
return
_extract_piece_states(piece_meta["pieces"], piece.get_node("Pieces"),
new_names)
if piece is PieceContainer:
piece.recalculate_mass()
elif type_key == "pieces":
if piece is Card:
# The state can choose not to have this data.
if piece_meta.has("is_collisions_on"):
if not piece_meta["is_collisions_on"] is bool:
push_error("Card " + old_piece_name + " collisions on is not a boolean!")
return
piece.set_collisions_on(piece_meta["is_collisions_on"])
elif type_key == "stacks":
for stack_piece_meta in piece_meta["pieces"]:
if not stack_piece_meta is Dictionary:
push_error("Stack piece is not a dictionary!")
return
if not stack_piece_meta.has("flip_y"):
push_error("Stack piece does not have a flip value!")
return
if not stack_piece_meta["flip_y"] is bool:
push_error("Stack piece flip value is not a boolean!")
return
if not stack_piece_meta.has("entry_path"):
push_error("Stack piece does not have an entry path!")
return
if not stack_piece_meta["entry_path"] is String:
push_error("Stack piece entry path is not a string!")
return
var stack_piece_entry_path = stack_piece_meta["entry_path"]
var stack_piece_entry = AssetDB.search_path(stack_piece_entry_path)
if not stack_piece_entry.empty():
# Add it to the stack at the top (since we're going through the
# list in order from bottom to top).
var flip = Stack.FLIP_NO
if stack_piece_meta["flip_y"]:
flip = Stack.FLIP_YES
piece.add_piece(stack_piece_entry, Transform.IDENTITY,
Stack.STACK_TOP, flip)
else:
push_error("Cannot add piece to stack, entry not found: %s" % stack_piece_entry_path)
elif type_key == "speakers" or type_key == "timers":
if not piece_meta.has("is_music_track"):
push_error("Speaker " + old_piece_name + " does not have an is music track value!")
return
if not piece_meta["is_music_track"] is bool:
push_error("Speaker " + old_piece_name + " is music track value is not a boolean!")
return
if not piece_meta.has("is_playing"):
push_error("Speaker " + old_piece_name + " does not have an is playing value!")
return
if not piece_meta["is_playing"] is bool:
push_error("Speaker " + old_piece_name + " is playing value is not a boolean!")
return
if not piece_meta.has("is_positional"):
push_warning("Speaker %s does not have key 'is_positional', defaulting to false." % old_piece_name)
piece_meta["is_positional"] = false
if not piece_meta["is_positional"] is bool:
push_error("Speaker %s 'is_positional' value is not a boolean!" % old_piece_name)
return
if not piece_meta.has("is_track_paused"):
push_error("Speaker " + old_piece_name + " does not have an is track paused value!")
return
if not piece_meta["is_track_paused"] is bool:
push_error("Speaker " + old_piece_name + " is track paused value is not a boolean!")
return
if not piece_meta.has("playback_position"):
push_error("Speaker " + old_piece_name + " does not have a playback position value!")
return
if not piece_meta["playback_position"] is float:
push_error("Speaker " + old_piece_name + " playback position value is not a float!")
return
if not piece_meta.has("track_entry"):
push_error("Speaker " + old_piece_name + " does not have a track entry!")
return
if not piece_meta["track_entry"] is Dictionary:
push_error("Speaker " + old_piece_name + " track entry is not a dictionary!")
return
if not piece_meta.has("unit_size"):
push_error("Speaker " + old_piece_name + " does not have a unit size value!")
return
if not piece_meta["unit_size"] is float:
push_error("Speaker " + old_piece_name + " unit size value is not a float!")
return
if piece is SpeakerPiece:
piece.set_positional(piece_meta["is_positional"])
piece.set_track(piece_meta["track_entry"], piece_meta["is_music_track"])
piece.set_unit_size(piece_meta["unit_size"])
if piece_meta["is_playing"]:
piece.play_track(piece_meta["playback_position"])
if piece_meta["is_track_paused"]:
piece.pause_track(piece_meta["playback_position"])
if type_key == "timers":
if not piece_meta.has("is_timer_paused"):
push_error("Timer " + old_piece_name + " does not have an is timer paused value!")
return
if not piece_meta["is_timer_paused"] is bool:
push_error("Timer " + old_piece_name + " is timer paused value is not a boolean!")
return
if not piece_meta.has("mode"):
push_error("Timer " + old_piece_name + " does not have a mode value!")
return
if not piece_meta["mode"] is int:
push_error("Timer " + old_piece_name + " mode value is not an integer!")
return
if not piece_meta.has("time"):
push_error("Timer " + old_piece_name + " does not have a time value!")
return
if not piece_meta["time"] is float:
push_error("Timer " + old_piece_name + " time value is not a float!")
return
if piece is TimerPiece:
piece.set_mode(piece_meta["mode"])
if piece_meta["is_timer_paused"]:
piece.pause_timer_at(piece_meta["time"])
else:
piece.set_time(piece_meta["time"])
# Finally, we may need to move the piece in the scene tree so it has a
# different parent.
if parent != _pieces:
_pieces.remove_child(piece)
parent.add_child(piece)
# Clean the pieces that have been put into limbo from memory.
# force: If true, clean all pieces. If false, only clean pieces that have been
# in limbo for a certain amount of time.
func _limbo_clean_pieces(force: bool) -> void:
for piece in get_tree().get_nodes_in_group("limbo"):
var entered_limbo: int = 0
if piece.has_meta("entered_limbo"):
entered_limbo = piece.get_meta("entered_limbo")
var duration_in_limbo = OS.get_ticks_msec() - entered_limbo
if force or duration_in_limbo > LIMBO_DURATION_MS:
_pieces.remove_child(piece)
ResourceManager.queue_free_object(piece)
# Modify piece states such that the pieces have new names, and their positions
# are offset by a given amount.
# state: The state to modify.
# offset: How much to offset the piece's positions by.
func _modify_piece_states(state: Dictionary, offset: Vector3) -> void:
for type in state:
var type_dict = state[type]
if type_dict is Dictionary:
var names = type_dict.keys()
for name in names:
var piece_meta = type_dict[name]
# If the piece is a container, we need to also modify the
# container's contents.
if type == "containers":
if piece_meta.has("pieces"):
var pieces = piece_meta["pieces"]
if pieces is Dictionary:
_modify_piece_states(pieces, Vector3.ZERO)
# Offset the position.
if piece_meta.has("transform"):
var piece_transform = piece_meta["transform"]
if piece_transform is Transform:
piece_transform.origin += offset
piece_meta["transform"] = piece_transform
# Set the transform of a hidden area based on two corner points.
# hidden_area: The hidden area to set the transform of.
# point1: One corner.
# point2: The opposite corner.
func _set_hidden_area_transform(hidden_area: HiddenArea, point1: Vector2, point2: Vector2) -> void:
var min_point = Vector2(min(point1.x, point2.x), min(point1.y, point2.y))
var max_point = Vector2(max(point1.x, point2.x), max(point1.y, point2.y))
var avg_point = 0.5 * (min_point + max_point)
var point_dif = max_point - min_point
hidden_area.transform.origin.x = avg_point.x
hidden_area.transform.origin.z = avg_point.y
# We're assuming here that the hidden area is never rotated.
hidden_area.transform.basis.x.x = point_dif.x / 2
hidden_area.transform.basis.z.z = point_dif.y / 2
# Disable the creation of undo states.
# NOTE: Using this function multiple times in the call stack will increment a
# counter, and each invocation needs to be re-enabled for undo states to be
# created again.
func _srv_undo_state_creation_disable() -> void:
_srv_undo_disable_state_creation += 1
# Re-enable the creation of undo states.
func _srv_undo_state_creation_enable() -> void:
_srv_undo_disable_state_creation -= 1
if _srv_undo_disable_state_creation < 0:
_srv_undo_disable_state_creation = 0
# Update the rate at which state updates are sent to the client, based on the
# number of active pieces in the room.
func _srv_update_bandwidth_throttle() -> void:
# TODO: Instead of using the total number of pieces, use the number of
# pieces that are NOT sleeping. This could be done with
# PhysicsServer.get_process_info, but it has not been implemented with the
# Bullet physics engine.
# See: https://github.com/godotengine/godot/issues/59279
var physics_frames_per_update = floor(1.0 + _pieces.get_child_count() / Global.SRV_PIECE_UPDATE_TRANSMIT_LIMIT)
if Global.srv_num_physics_frames_per_state_update != physics_frames_per_update:
Global.srv_num_physics_frames_per_state_update = physics_frames_per_update
print("State update rate set to %.2fHz." % (60 / physics_frames_per_update))
func _on_container_absorbing_hovered(container: PieceContainer, player_id: int) -> void:
if get_tree().is_network_server():
var names = []
var main: Piece = null
# TODO: Optimize this by using groups?
for piece in _pieces.get_children():
if piece is Piece:
if piece.hover_player == player_id:
names.append(piece.name)
if piece.hover_offset == Vector3.ZERO:
main = piece
if main != null:
# If there is a lot of space inbetween the container and the main
# piece the player is hovering, then it's possible that another
# piece bumped into the container by accident, so only add the
# pieces is the main piece is close to the container.
var distance = (container.transform.origin - main.transform.origin).length()
var container_radius = container.get_radius()
var piece_radius = main.get_radius()
var space = distance - container_radius - piece_radius
if space > 15.0:
return
request_add_pieces_to_container(container.name, names)
func _on_container_releasing_random_piece(container: PieceContainer) -> void:
if get_tree().is_network_server():
rpc_id(1, "request_container_release_random", container.name, 1, false)
func _on_stack_requested(piece1: StackablePiece, piece2: StackablePiece) -> void:
if not get_tree().is_network_server():
return
if not _srv_allow_card_stacking and (piece1 is Card or piece2 is Card):
return
if piece1.is_in_group("limbo") or piece2.is_in_group("limbo"):
return
if piece1 is Stack and piece2 is Stack:
rpc("add_stack_to_stack", piece1.name, piece2.name, piece1.transform,
piece2.transform)
elif piece1 is Stack:
rpc("add_piece_to_stack", piece2.name, piece1.name, piece2.transform,
piece1.transform)
elif piece2 is Stack:
rpc("add_piece_to_stack", piece1.name, piece2.name, piece1.transform,
piece2.transform)
else:
rpc("add_stack", srv_get_next_piece_name(), piece1.name, piece2.name,
piece1.transform, piece2.transform)
# Called by the server when the undo stack is empty.
remotesync func _on_undo_stack_empty():
emit_signal("undo_stack_empty")
# Called by the server when the undo stack is pushed to.
remotesync func _on_undo_stack_pushed():
emit_signal("undo_stack_pushed")
func _on_CameraController_adding_cards_to_hand(cards: Array, id: int):
var names = []
for card in cards:
if card.get("over_hands") != null:
names.append(card.name)
if id > 0:
rpc_id(1, "request_add_cards_to_hand", names, id)
else:
rpc_id(1, "request_add_cards_to_nearest_hand", names)
func _on_CameraController_adding_pieces_to_container(container: PieceContainer, pieces: Array):
var piece_names = []
for piece in pieces:
if piece != container:
piece_names.append(piece.name)
rpc_id(1, "request_add_pieces_to_container", container.name, piece_names)
func _on_CameraController_clipboard_paste(position: Vector3):
# If we are the server, then duplicate the clipboard contents, as the
# request will modify the contents by reference otherwise.
var clipboard = _camera_controller.clipboard_contents
if get_tree().is_network_server():
clipboard = clipboard.duplicate(true)
var offset = position - _camera_controller.clipboard_yank_position
rpc_id(1, "request_paste_clipboard", clipboard, offset)
func _on_CameraController_clipboard_yank(pieces: Array):
_append_piece_states(_camera_controller.clipboard_contents, pieces, false)
func _on_CameraController_collect_pieces_requested(pieces: Array):
var names = []
for piece in pieces:
if piece is StackablePiece:
names.append(piece.name)
rpc_id(1, "request_collect_pieces", names)
func _on_CameraController_container_release_random_requested(container: PieceContainer, n: int):
rpc_id(1, "request_container_release_random", container.name, n, true)
func _on_CameraController_container_release_these_requested(container: PieceContainer, names: Array):
var good_names = []
for check_name in names:
if check_name is String:
if container.has_piece(check_name):
good_names.append(check_name)
rpc_id(1, "request_container_release_these", container.name, good_names, true)
func _on_CameraController_dealing_cards(stack: Stack, n: int):
rpc_id(1, "request_deal_cards", stack.name, n)
func _on_CameraController_erasing(pos1: Vector3, pos2: Vector3, size: float):
_paint_plane.rpc_unreliable_id(1, "request_push_paint_queue", pos1, pos2,
Color.transparent, size)
func _on_CameraController_hover_piece_requested(piece: Piece, offset: Vector3):
rpc_id(1, "request_hover_piece", piece.name,
_camera_controller.get_hover_position(), offset)
func _on_CameraController_hover_pieces_requested(pieces: Array, offsets: Array):
var names = []
for piece in pieces:
if piece is Piece:
names.append(piece.name)
if names.size() != offsets.size():
push_error("Name and offset arrays differ in size (name = %d, offset = %d)!" % [names.size(), offsets.size()])
return
rpc_id(1, "request_hover_pieces", names,
_camera_controller.get_hover_position(), offsets)
func _on_CameraController_painting(pos1: Vector3, pos2: Vector3, color: Color, size: float):
_paint_plane.rpc_unreliable_id(1, "request_push_paint_queue", pos1, pos2,
color, size)
func _on_CameraController_placing_hidden_area(point1: Vector2, point2: Vector2):
var size = (point2 - point1).abs()
if size.x < HIDDEN_AREA_MIN_SIZE.x or size.y < HIDDEN_AREA_MIN_SIZE.y:
return
rpc_id(1, "request_place_hidden_area", point1, point2)
func _on_CameraController_pop_stack_requested(stack: Stack, n: int):
rpc_id(1, "request_pop_stack", stack.name, n, true, 1.0)
func _on_CameraController_removing_hidden_area(hidden_area: HiddenArea):
if _hidden_areas.is_a_parent_of(hidden_area):
rpc_id(1, "request_remove_hidden_area", hidden_area.name)
func _on_CameraController_removing_pieces(pieces: Array):
var piece_names = []
for piece in pieces:
if piece is Piece:
piece_names.append(piece.name)
rpc_id(1, "request_remove_pieces", piece_names)
func _on_CameraController_selecting_all_pieces():
var pieces = _pieces.get_children()
_camera_controller.append_selected_pieces(pieces)
func _on_CameraController_setting_hidden_area_preview_points(point1: Vector2, point2: Vector2):
_set_hidden_area_transform(_hidden_area_preview, point1, point2)
func _on_CameraController_setting_hidden_area_preview_visible(is_visible: bool):
_hidden_area_preview.visible = is_visible
_hidden_area_preview.collision_layer = 1 if is_visible else 2
func _on_CameraController_setting_hover_position_multiple(position: Vector3):
rpc_unreliable_id(1, "set_hover_position_multiple", position)
func _on_CameraController_setting_spawn_point(position: Vector3):
emit_signal("setting_spawn_point", position)
func _on_CameraController_spawning_fast_circle():
if not _fast_circle.is_processing():
rpc_id(1, "request_spawn_fast_circle")
func _on_CameraController_spawning_piece_at(position: Vector3):
emit_signal("spawning_piece_at", position)
func _on_CameraController_spawning_piece_in_container(container_name: String):
emit_signal("spawning_piece_in_container", container_name)
func _on_CameraController_stack_collect_all_requested(stack: Stack, collect_stacks: bool):
rpc_id(1, "request_stack_collect_all", stack.name, collect_stacks)
func _on_GameUI_clear_paint():
if not _paint_plane.is_inside_tree():
return
_paint_plane.rpc_id(1, "request_clear_paint")
func _on_GameUI_clear_pieces():
var piece_names = []
for piece in _pieces.get_children():
if piece is Piece:
piece_names.append(piece.name)
rpc_id(1, "request_remove_pieces", piece_names)
for hidden_area in _hidden_areas.get_children():
if hidden_area is HiddenArea:
rpc_id(1, "request_remove_hidden_area", hidden_area.name)
func _on_GameUI_rotation_amount_updated(rotation_amount: float):
_camera_controller.set_piece_rotation_amount(rotation_amount)
func _on_GameUI_undo_state():
rpc_id(1, "pop_undo_state")
func _on_LimboCleanTimer_timeout():
call_deferred("_limbo_clean_pieces", false)
func _on_Lobby_player_added(incoming_id: int):
if get_tree().is_network_server() and incoming_id != 1:
# Players joining the game will need to be aware of any hovering pieces.
for piece in _pieces.get_children():
if piece is Piece:
if piece.hover_player > 0:
piece.rpc_id(incoming_id, "start_hovering", piece.hover_player,
piece.hover_position, piece.hover_offset)
for player_id in _client_hover_pieces:
rpc_id(incoming_id, "set_client_hover_pieces", player_id,
_client_hover_pieces[player_id])
func _on_Room_tree_exiting():
if not _paint_plane.get_parent():
_paint_plane.free()