mirror of
https://github.com/drwhut/tabletop-club.git
synced 2025-05-05 15:32:56 +00:00
351 lines
10 KiB
GDScript
351 lines
10 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 Node
|
|
|
|
signal censor_changed()
|
|
|
|
enum {
|
|
MODE_NONE,
|
|
MODE_ERROR,
|
|
MODE_CLIENT,
|
|
MODE_SERVER,
|
|
MODE_SINGLEPLAYER
|
|
}
|
|
|
|
const LOADING_BLOCK_TIME = 20
|
|
|
|
# From the tabletop_club_godot_module:
|
|
# https://github.com/drwhut/tabletop_club_godot_module
|
|
var tabletop_importer = null
|
|
var error_reporter = null
|
|
|
|
var system_locale: String = ""
|
|
|
|
var censoring_profanity: bool = true setget set_censoring_profanity
|
|
|
|
# Throttle the piece state transmissions if there are many active physics
|
|
# objects.
|
|
const SRV_PIECE_UPDATE_TRANSMIT_LIMIT = 20
|
|
var srv_num_physics_frames_per_state_update: int = 1
|
|
|
|
# Do not send state updates to players if they are not ready.
|
|
var srv_state_update_blacklist: Array = []
|
|
|
|
var _current_scene: Node = null
|
|
var _loader: ResourceInteractiveLoader = null
|
|
var _loader_args: Dictionary = {}
|
|
var _wait_frames = 0
|
|
|
|
var _profanity_list: Array = []
|
|
|
|
# Given a string, return a new string with profanity hidden.
|
|
# Returns: A (hopefully) profanity-less string.
|
|
# string: The string to censor.
|
|
func censor_profanity(string: String) -> String:
|
|
var lower_string = string.to_lower()
|
|
|
|
# We are assuming the profanity list is in alphabetical order, and thus
|
|
# prefixes will come first.
|
|
for i in range(_profanity_list.size() - 1, -1, -1):
|
|
var profanity = _profanity_list[i].to_lower()
|
|
var j = 0
|
|
while j >= 0:
|
|
j = lower_string.find_last(profanity)
|
|
if j >= 0:
|
|
var censor = false
|
|
if lower_string.length() == profanity.length():
|
|
censor = true
|
|
elif j == 0:
|
|
# Most control and punctuation characters are below 65.
|
|
if lower_string.ord_at(profanity.length()) < 65:
|
|
censor = true
|
|
elif j == lower_string.length() - profanity.length():
|
|
if lower_string.ord_at(j - 1) < 65:
|
|
censor = true
|
|
else:
|
|
if lower_string.ord_at(j - 1) < 65 and lower_string.ord_at(j + profanity.length()) < 65:
|
|
censor = true
|
|
|
|
if censor:
|
|
string.erase(j, profanity.length())
|
|
string = string.insert(j, "*".repeat(profanity.length()))
|
|
lower_string = lower_string.substr(0, j)
|
|
|
|
return string
|
|
|
|
# Get the directory of the given subfolder in the output folder. This should be
|
|
# in the user's documents folder, but if it isn't, the function will resort to
|
|
# the user:// folder instead.
|
|
# Returns: The directory of the subfolder in the output folder.
|
|
# subfolder: The subfolder to get the directory of.
|
|
func get_output_subdir(subfolder: String) -> Directory:
|
|
var dir = Directory.new()
|
|
|
|
if dir.open(OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)) != OK:
|
|
if dir.open("user://") != OK:
|
|
push_error("Could not open the output directory!")
|
|
return dir
|
|
|
|
if not dir.dir_exists("TabletopClub"):
|
|
if dir.make_dir("TabletopClub") != OK:
|
|
push_error("Failed to create the output directory!")
|
|
return dir
|
|
|
|
if dir.change_dir("TabletopClub") != OK:
|
|
push_error("Failed to change to the output directory!")
|
|
return dir
|
|
|
|
if not dir.dir_exists(subfolder):
|
|
if dir.make_dir_recursive(subfolder) != OK:
|
|
push_error("Failed to create the '%s' subfolder!" % subfolder)
|
|
return dir
|
|
|
|
if dir.change_dir(subfolder) != OK:
|
|
push_error("Failed to change to the '%s' subfolder!" % subfolder)
|
|
return dir
|
|
|
|
return dir
|
|
|
|
# Rotate the volume of a bounding box to create a new bounding box.
|
|
func rotate_bounding_box(size: Vector3, basis: Basis) -> Vector3:
|
|
var corner_0 = basis * size
|
|
var corner_x = (corner_0 - 2 * size.x * basis.x).abs()
|
|
var corner_y = (corner_0 - 2 * size.y * basis.y).abs()
|
|
var corner_z = (corner_0 - 2 * size.z * basis.z).abs()
|
|
corner_0 = corner_0.abs()
|
|
|
|
var max_x = max(corner_0.x, corner_x.x)
|
|
max_x = max(max_x, corner_y.x)
|
|
max_x = max(max_x, corner_z.x)
|
|
|
|
var max_y = max(corner_0.y, corner_x.y)
|
|
max_y = max(max_y, corner_y.y)
|
|
max_y = max(max_y, corner_z.y)
|
|
|
|
var max_z = max(corner_0.z, corner_x.z)
|
|
max_z = max(max_z, corner_y.z)
|
|
max_z = max(max_z, corner_z.z)
|
|
|
|
return Vector3(max_x, max_y, max_z)
|
|
|
|
# Set whether the game will censor profanity in player-generated text.
|
|
# censor: If the game will censor profanity.
|
|
func set_censoring_profanity(censor: bool) -> void:
|
|
var emit_after = (censor != censoring_profanity)
|
|
censoring_profanity = censor
|
|
|
|
if emit_after:
|
|
emit_signal("censor_changed")
|
|
|
|
# Start the game as a client.
|
|
# room_code: The room code to connect to.
|
|
func start_game_as_client(room_code: String) -> void:
|
|
_goto_scene("res://Scenes/Game/Game.tscn", {
|
|
"mode": MODE_CLIENT,
|
|
"room_code": room_code
|
|
})
|
|
|
|
# Start the game as a server.
|
|
func start_game_as_server() -> void:
|
|
_goto_scene("res://Scenes/Game/Game.tscn", {
|
|
"mode": MODE_SERVER
|
|
})
|
|
|
|
# Start the game in singleplayer mode.
|
|
func start_game_singleplayer() -> void:
|
|
_goto_scene("res://Scenes/Game/Game.tscn", {
|
|
"mode": MODE_SINGLEPLAYER
|
|
})
|
|
|
|
# Start the importing assets scene.
|
|
func start_importing_assets() -> void:
|
|
call_deferred("_terminate_peer")
|
|
_goto_scene("res://Scenes/ImportAssets.tscn", {
|
|
"mode": MODE_NONE
|
|
})
|
|
|
|
# Start the main menu.
|
|
func start_main_menu() -> void:
|
|
call_deferred("_terminate_peer")
|
|
_goto_scene("res://Scenes/MainMenu.tscn", {
|
|
"mode": MODE_NONE
|
|
})
|
|
|
|
# Start the main menu, and display an error message.
|
|
# error: The error message to display.
|
|
func start_main_menu_with_error(error: String) -> void:
|
|
call_deferred("_terminate_peer")
|
|
_goto_scene("res://Scenes/MainMenu.tscn", {
|
|
"mode": MODE_ERROR,
|
|
"error": error
|
|
})
|
|
|
|
func _ready():
|
|
var root = get_tree().get_root()
|
|
_current_scene = root.get_child(root.get_child_count() - 1)
|
|
|
|
set_process(false)
|
|
|
|
# We're assuming the locale hasn't been modified yet.
|
|
system_locale = TranslationServer.get_locale()
|
|
|
|
# We may be running the game with vanilla Godot!
|
|
if type_exists("TabletopImporter"):
|
|
tabletop_importer = ClassDB.instance("TabletopImporter")
|
|
if type_exists("ErrorReporter"):
|
|
error_reporter = ClassDB.instance("ErrorReporter")
|
|
|
|
_profanity_list = preload("res://Text/Profanity.tres").text.split("\n", false)
|
|
|
|
func _process(_delta):
|
|
if _loader == null:
|
|
set_process(false)
|
|
return
|
|
|
|
if _wait_frames > 0:
|
|
_wait_frames -= 1
|
|
return
|
|
|
|
var time = OS.get_ticks_msec()
|
|
while OS.get_ticks_msec() < time + LOADING_BLOCK_TIME:
|
|
var err = _loader.poll()
|
|
|
|
if err == ERR_FILE_EOF:
|
|
var scene = _loader.get_resource()
|
|
call_deferred("_set_scene", scene.instance(), _loader_args)
|
|
|
|
_loader = null
|
|
_loader_args = {}
|
|
break
|
|
elif err == OK:
|
|
# The current scene should be the loading scene, so we should be
|
|
# able to update the progress it is displaying.
|
|
var progress = 0.0
|
|
var stages = _loader.get_stage_count()
|
|
if stages > 0:
|
|
progress = float(_loader.get_stage()) / stages
|
|
_current_scene.set_progress(progress)
|
|
else:
|
|
push_error("Loader encountered an error (error code %d)!" % err)
|
|
_loader = null
|
|
break
|
|
|
|
# Go to a given scene, with a set of arguments.
|
|
# path: The file path of the scene to load.
|
|
# args: The arguments for the scene to use after it has loaded.
|
|
func _goto_scene(path: String, args: Dictionary) -> void:
|
|
# Are we already loading a scene?
|
|
if _loader != null:
|
|
return
|
|
|
|
# Create the interactive loader for the new scene.
|
|
_loader = ResourceLoader.load_interactive(path)
|
|
if _loader == null:
|
|
push_error("Failed to create loader for '%s'!" % path)
|
|
return
|
|
_loader_args = args
|
|
|
|
# Load the loading scene so the player can see the progress in loading the
|
|
# new scene.
|
|
var loading_scene = preload("res://Scenes/Loading.tscn").instance()
|
|
call_deferred("_set_scene", loading_scene, { "mode": MODE_NONE })
|
|
|
|
set_process(true)
|
|
_wait_frames = 1
|
|
|
|
# Immediately set the scene tree's current scene.
|
|
# NOTE: This function should be called via call_deferred, since it will free
|
|
# the existing scene.
|
|
# scene: The scene to load.
|
|
# args: The arguments for the scene to use after it has loaded.
|
|
func _set_scene(scene: Node, args: Dictionary) -> void:
|
|
if not args.has("mode"):
|
|
push_error("Scene argument 'mode' is missing!")
|
|
return
|
|
|
|
if not args["mode"] is int:
|
|
push_error("Scene argument 'mode' is not an integer!")
|
|
return
|
|
|
|
match args["mode"]:
|
|
MODE_NONE:
|
|
pass
|
|
|
|
MODE_ERROR:
|
|
if not args.has("error"):
|
|
push_error("Scene argument 'error' is missing!")
|
|
return
|
|
|
|
if not args["error"] is String:
|
|
push_error("Scene argument 'error' is not a string!")
|
|
return
|
|
|
|
MODE_CLIENT:
|
|
if not args.has("room_code"):
|
|
push_error("Scene argument 'room_code' is missing!")
|
|
return
|
|
|
|
if not args["room_code"] is String:
|
|
push_error("Scene argument 'room_code' is not a string!")
|
|
return
|
|
|
|
MODE_SERVER:
|
|
pass
|
|
|
|
MODE_SINGLEPLAYER:
|
|
pass
|
|
|
|
_:
|
|
push_error("Invalid mode " + str(args["mode"]) + "!")
|
|
return
|
|
|
|
var root = get_tree().get_root()
|
|
|
|
# Free the current scene - this should not be done during the main loop!
|
|
root.remove_child(_current_scene)
|
|
_current_scene.free()
|
|
|
|
AssetDB.clear_temp_db()
|
|
|
|
root.add_child(scene)
|
|
get_tree().set_current_scene(scene)
|
|
_current_scene = scene
|
|
|
|
match args["mode"]:
|
|
MODE_ERROR:
|
|
_current_scene.display_error(args["error"])
|
|
MODE_CLIENT:
|
|
_current_scene.start_join(args["room_code"])
|
|
MODE_SERVER:
|
|
_current_scene.start_host()
|
|
MODE_SINGLEPLAYER:
|
|
_current_scene.start_singleplayer()
|
|
|
|
srv_num_physics_frames_per_state_update = 1
|
|
|
|
# Terminate the network peer if it exists.
|
|
# NOTE: This function should be called via call_deferred.
|
|
func _terminate_peer() -> void:
|
|
# TODO: Send a message to say we are leaving first.
|
|
get_tree().network_peer = null
|