diff --git a/src/addons/opensusinteraction/opensusinteraction.gd b/src/addons/opensusinteraction/opensusinteraction.gd index 48e9d500..6e907352 100644 --- a/src/addons/opensusinteraction/opensusinteraction.gd +++ b/src/addons/opensusinteraction/opensusinteraction.gd @@ -23,11 +23,11 @@ func _enter_tree(): #add custom resources add_custom_type("Interact", "Resource", interact_resource_script, object_icon) add_custom_type("InteractMap", "Resource", interactmap_resource_script, object_icon) - add_custom_type("InteractTask", "Resource", task_resource_script, object_icon) + #add_custom_type("InteractTask", "Resource", task_resource_script, object_icon) add_custom_type("InteractUI", "Resource", interactui_resource_script, object_icon) func _exit_tree(): remove_custom_type("Interact") remove_custom_type("InteractMap") - remove_custom_type("InteractTask") + #remove_custom_type("InteractTask") remove_custom_type("InteractUI") diff --git a/src/addons/opensusinteraction/resources/interact/interact.tres b/src/addons/opensusinteraction/resources/interact/interact.tres index 30c3965d..5a671e2d 100644 --- a/src/addons/opensusinteraction/resources/interact/interact.tres +++ b/src/addons/opensusinteraction/resources/interact/interact.tres @@ -16,7 +16,7 @@ interact_data = { } advanced/network_sync = false -[sub_resource type="Resource" id=2] +[sub_resource type="Resource" id=4] resource_local_to_scene = true resource_name = "InteractUI" script = ExtResource( 4 ) @@ -33,8 +33,10 @@ resource_local_to_scene = true resource_name = "InteractTask" script = ExtResource( 5 ) task_text = "" -ui_resource = SubResource( 2 ) +ui_resource = SubResource( 4 ) outputs/toggle_map_interactions = false +outputs/output_map_interactions = [ ] +is_task_global = false [resource] resource_local_to_scene = true diff --git a/src/addons/opensusinteraction/resources/interacttask/interacttask.gd b/src/addons/opensusinteraction/resources/interacttask/interacttask.gd index 71f8d69b..0f6e2df8 100644 --- a/src/addons/opensusinteraction/resources/interacttask/interacttask.gd +++ b/src/addons/opensusinteraction/resources/interacttask/interacttask.gd @@ -1,12 +1,10 @@ tool extends Resource -#class_name InteractTask +class_name InteractTask export(String) var task_text -export(int) var random_numbers = 0 - var item_inputs_on: bool var item_inputs: PoolStringArray @@ -31,13 +29,17 @@ var ui_res: Resource = base_ui_resource.duplicate() #node this task is attached to var attached_to: Node -#assigned by a programmer when added to the scene -#needs to be unique -export var task_id: int = TaskManager.INVALID_TASK_ID +var task_id: int = TaskManager.INVALID_TASK_ID var task_data: Dictionary = {} var task_data_player: Dictionary = {} var task_registered: bool = false +var network_master: int = 1 +# function name: rpc mode (puppet, remote, etc.) +var networked_functions: Dictionary = {} +# var name: rpc mode (puppet, remote, etc.) +var networked_properties: Dictionary = {} + # relationships between shown property names and the actual script property name # properties in this dict are NOT automatically added to editor, they must also be in custom_properties_to_show # if you want the editor property name to be the same as the script variable name, you do not need to add it to custom_properties @@ -62,58 +64,152 @@ var custom_properties: Dictionary = { # if you want the editor property name to be the same as the script variable name, you do not need to add it to custom_properties var custom_properties_to_show: PoolStringArray = ["ui_resource", "outputs/toggle_map_interactions", "outputs/output_map_interactions", "is_task_global"] -func complete_task( player_id: int = TaskManager.GLOBAL_TASK_PLAYER_ID, - data: Dictionary = {}) -> bool: +signal transitioned(old_state, new_state, player_id) +signal task_completed(player_id, data) - var temp_interact_data = task_data_player[player_id] +# should be called by task UI or an extending script to complete the task +# this function should not add any actual behavior, just relay the attempt to +# complete the task to TaskManager +func complete_task( player_id: int = Network.get_my_id(), + data: Dictionary = {}) -> void: + if not can_complete_task(player_id, data): + return + # use virtual function to add custom behavior while retaining the checks or to + # cancel the attempt to complete this task + # this is similar behavior to assign_player(), registered(), gen_task_data(), etc. + # if you want fully custom behavior, override this function instead + if _complete_task(player_id, data) == false: + return + var task_info: Dictionary = TaskManager.gen_task_info(get_task_id(), player_id) + var new_data: Dictionary = Helpers.merge_dicts(data, get_task_data(player_id)) + TaskManager.complete_task(task_info, new_data) + +# override to add custom behavior when an attempt is made to complete the task or to +# cancel the attempt +func _complete_task(player_id: int, data: Dictionary): + pass + +# mainly serves as a constant function the TaskManager can call to see if the task is +# unofficially completed (all requirements are met, like the time being set correctly +# in the clockset task), custom checks can be added in _can_complete_task() +func can_complete_task(player_id: int = Network.get_my_id(), data: Dictionary = {}) -> bool: + if not is_player_assigned(player_id): + return false + if is_task_completed(player_id): + return false + var virt_return = _can_complete_task(player_id, data) + if not virt_return is bool: + return true + return virt_return + +# while overriding, you must return a bool (whether or not the task will be completed) which +# will be relayed to whatever called can_complete_task() (most likely TaskManager) +# this is to add custom checks to see if the task is completed or not +func _can_complete_task(player_id: int, data: Dictionary) -> bool: + # returns true by default to allow you to control a task purely through the UI script + # to avoid having to make a custom resource. This makes simple tasks much easier + # to make and reduces unnecessary scripts + return true + +# called when a task completion is verified by the server, in Among Us the delay in this step +# is hidden by having the UI stay open until the confirmation is received. I believe this +# is what currently happens in opensuspect as of 2/18/2021 - TheSecondReal0 +# task must be completed somehow during this function, to avoid desync +func task_completed(player_id: int, data: Dictionary): + # if nothing is explicitly returned, _task_completed() will return null and will not trigger this + # this allows an extending script to override behavior while retaining the above checks + # by defining _task_completed() and returning false. If you want fully custom + # behavior, override this function instead + if _task_completed(player_id, data) == false: + return + transition(TaskManager.task_state.COMPLETED, player_id) + var temp_interact_data = get_task_data(player_id) for key in data.keys(): temp_interact_data[key] = data[key] if map_outputs_on: for resource in map_outputs: resource.interact(attached_to, temp_interact_data) - return true + emit_signal("task_completed", player_id, temp_interact_data) +# complete_task(player_id, data) -func assign_player(player_id: int = TaskManager.GLOBAL_TASK_PLAYER_ID): - +# overridden to add custom behavior when the task is completed +# return false while overriding to break execution of task_completed() +func _task_completed(player_id: int, data: Dictionary): + pass + +func assign_player(player_id: int, data: Dictionary = {}): if task_data_player.has(player_id): return - task_data_player[player_id] = task_data.duplicate(true) + if get_tree().is_network_server(): + task_data_player[player_id] = gen_player_task_data(player_id) + else: + task_data_player[player_id] = data var task_text = task_data["task_text"] - var data = [] - assert(random_numbers >= 0) - randomize() - for i in range(random_numbers): - data.append(randi()) - #var data: Dictionary = TaskGenerators.call_generator(task_text) - task_data_player[player_id]["task_data"] = data + # if nothing is explicitly returned, _assign_player() will return null and will not trigger this + # this allows an extending script to override behavior while retaining the above checks + # by defining _assign_player() and returning false. If you want fully custom + # behavior, override this function instead + # used to add custom behavior when a player is assigned to this task + if _assign_player(player_id, data) == false: + return + +# overridden to add custom behavior for when a player is assigned to this task while +# retaining the checks implemented in assign_player() +# return false to break out of assign_player() early (before any actions are taken) +func _assign_player(player_id: int, data: Dictionary): + pass +# called by the task manager when it has registered this task func registered(new_task_id: int, new_task_data: Dictionary): + _registered(new_task_id, new_task_data) for key in new_task_data.keys(): task_data[key] = new_task_data[key] task_id = new_task_id task_registered = true +# override to add custom behavior upon being registered by the task manager +func _registered(new_task_id: int, new_task_data: Dictionary): + pass + +# while this function doesn't add any functionality, it does provide a constant +# function to call, and we could easily add checks later +# I'm thinking that this could be called by task UIs +func sync_task(): + _sync_task() + +# to be overridden by an extending script +func _sync_task(): + pass + +# used to get task data after the task has been registered func get_task_data(player_id: int = Network.get_my_id()) -> Dictionary: - if task_registered and is_task_global(): player_id = TaskManager.GLOBAL_TASK_PLAYER_ID - var temp_task_data = task_data + var temp_task_data = task_data.duplicate() if task_data_player.has(player_id): - temp_task_data = task_data_player[player_id] + temp_task_data = get_player_task_data(player_id).duplicate() temp_task_data["task_id"] = task_id - if task_registered: - return temp_task_data - var generated_task_data = gen_task_data() - for key in generated_task_data.keys(): - temp_task_data[key] = generated_task_data[key] + if not task_registered: + var generated_task_data = gen_task_data() + for key in generated_task_data.keys(): + temp_task_data[key] = generated_task_data[key] + + var virt_return: Dictionary = _get_task_data(player_id) + temp_task_data = Helpers.merge_dicts(temp_task_data, virt_return) + return temp_task_data +# override to add custom data when get_task_data() is called +func _get_task_data(player_id: int) -> Dictionary: + return {} + # generate initial data to send to the task manager, should not be called after it is registered +# this is the starting data used to sync tasks before the game starts func gen_task_data() -> Dictionary: - if task_registered: - return task_data +# if task_registered: +# return task_data var info: Dictionary = {} info["task_text"] = task_text # info["item_inputs"] = item_inputs @@ -123,49 +219,242 @@ func gen_task_data() -> Dictionary: info["resource"] = self info["is_task_global"] = is_task_global #info["ui_resource"] = ui_res + # so you can override _gen_task_data() and non-destructively add data + # if you want to remove data, you'd have to override this function + var virt_info: Dictionary = _gen_task_data() + for key in virt_info: + info[key] = virt_info[key] for key in info.keys(): task_data[key] = info[key] return info -func get_task_id() -> int: - return task_id - -func get_task_state(player_id: int = TaskManager.GLOBAL_TASK_PLAYER_ID) -> int: - if not task_data_player.has(player_id): - #this player has not been assigned this task - return TaskManager.task_state.HIDDEN - return task_data_player[player_id]["state"] +# meant to be overridden in an extending script to allow adding custom data to task_info +func _gen_task_data() -> Dictionary: + return {} -func set_task_state(player_id: int, new_state: int) -> bool: +# used to get player specific task data after the task has been registered and assigned +func get_player_task_data(player_id: int) -> Dictionary: + if task_registered and is_task_global(): + player_id = TaskManager.GLOBAL_TASK_PLAYER_ID + if not player_id in task_data_player: + return {} + var info: Dictionary = task_data_player[player_id] + var virt_return: Dictionary = _get_player_task_data(player_id) + var data: Dictionary = Helpers.merge_dicts(info, virt_return) + return data + +# override to add custom data when get_player_task_data() is called +func _get_player_task_data(player_id: int) -> Dictionary: + return {} + +# function called to generate player specific task data +func gen_player_task_data(player_id: int) -> Dictionary: + var info: Dictionary = task_data.duplicate(true) + var virt_return: Dictionary = _gen_player_task_data(player_id) + var data = Helpers.merge_dicts(info, virt_return) + return data + +# overridden by extending script to add player specific data to task_data_player upon generation +func _gen_player_task_data(player_id: int) -> Dictionary: + return {} + +func transition(new_state: int, player_id: int) -> bool: + # to add custom behavior/checks before the state is officially changed + # if _transition() returns false, interpret it to mean the extending script + # doesn't want to transition + # to remove behavior after this point, you must override this function (make sure + # to emit the "transitioned" signal) + var virt_return = _transition(new_state, player_id) + if virt_return is bool and virt_return == false: + return false + var old_state = task_data_player[player_id]["state"] task_data_player[player_id]["state"] = new_state + emit_signal("transitioned", old_state, new_state, player_id) return true -func is_task_global() -> bool: - return task_data["is_task_global"] - +# override to add custom behavior/checks before the state is officially changed +# this is especially useful if you want to cancel the transition as it would be +# much harder to retroactively undo it +# return false to break out of transition() early (this will also cause transition() +# to return false to whatever called it, most likely task manager or itself) +func _transition(new_state: int, player_id: int): + pass + func interact(_from: Node = null, _interact_data: Dictionary = {}): if attached_to == null and _from != null: attached_to = _from if attached_to == null: push_error("InteractTask resource trying to be used with no defined node") + if not is_player_assigned(Network.get_my_id()): + return + if is_task_completed(Network.get_my_id()): + return + # if nothing is explicitly returned, _interact() will return null and will not trigger this + # this check is so an extending script can override interact behavior (while retaining the + # above checks) by declaring _interact() and returning false. If you want fully custom + # behavior, override this function instead + # this could be used to cancel the interaction or to implement custom behavior, + # like triggering a map interaction instead of opening a UI + if _interact(_from, _interact_data) == false: + return ui_res.interact(_from, get_task_data()) +# meant to be overridden by an extending script to allow custom behavior when interacted with +func _interact(_from: Node = null, _interact_data: Dictionary = {}): + pass + func init_resource(_from: Node): if attached_to == null and _from != null: attached_to = _from if attached_to == null: push_error("InteractTask resource trying to be initiated with no defined node") + # if nothing is explicitly returned, _init_resource() will return null and will not trigger this + # this check is so an inheriting script can override initiation behavior while retaining the + # above checks. If you want fully custom behavior, override this function instead + # I can't think of any reason you wouldn't want to register with the task manager, + # but the ability to do so couldn't hurt + # calling the virtual function here also allows the extending script to add custom behavior + # before it is formally registered + if _init_resource(_from) == false: + return TaskManager.register_task(self) +# meant to be overridden by an extending script to allow custom behavior on resource init +func _init_resource(_from: Node): + pass + +func add_networked_func(function: String, rpc_mode: int): + networked_functions[function] = rpc_mode + +func add_networked_property(property: String, rpc_mode: int): + networked_properties[property] = rpc_mode + +# for consistency with using network functions in nodes +func set_network_master(id: int): + network_master = id + +func task_rset(property: String, value): + TaskManager.task_rset(property, value, task_id) + +func task_rset_id(id: int, property: String, value): + TaskManager.task_rset_id(id, property, value, task_id) + +func receive_task_rset(property: String, value): + if not property in networked_properties: + return + var sender: int = get_rpc_sender_id() + var rpc_mode: int = networked_properties[property] + if not is_valid_sender(sender, rpc_mode): + return + set(property, value) + +# args must be in the form of an array because you can't create functions with variable +# arg amounts in gdscript +func task_rpc(function: String, args: Array): + TaskManager.task_rpc(function, args, task_id) + +func task_rpc_id(id: int, function: String, args: Array): + TaskManager.task_rpc_id(id, function, args, task_id) + +func receive_task_rpc(function: String, args: Array): + if not function in networked_functions: + return + var sender: int = get_rpc_sender_id() + var rpc_mode: int = networked_functions[function] + if not is_valid_sender(sender, rpc_mode): + return + # using callv instead of call because it will translate the array into + # individual arguments + callv(function, args) + +# function to see if we should accept an rpc/rset +func is_valid_sender(sender: int, rpc_mode: int) -> bool: + var my_id: int = Network.get_my_id() + match rpc_mode: + MultiplayerAPI.RPC_MODE_REMOTE: + return my_id != sender + MultiplayerAPI.RPC_MODE_MASTER: + return my_id == network_master + MultiplayerAPI.RPC_MODE_PUPPET: + return my_id != network_master + MultiplayerAPI.RPC_MODE_REMOTESYNC: + return true + MultiplayerAPI.RPC_MODE_MASTERSYNC: + return my_id == network_master + MultiplayerAPI.RPC_MODE_PUPPETSYNC: + return my_id != network_master + return false + +# for consistency with using network functions in nodes +func get_rpc_sender_id() -> int: + return get_tree().get_rpc_sender_id() + +# to make it easier to access the scene tree to get specific data (like rpc sender id +# and to call is_network_server()) +func get_tree() -> SceneTree: + # going through TaskManager because resources do not have access to the scene tree + # by themselves + return TaskManager.get_tree() + +# not adding a virtual function for this because the same thing is accomplished by +# overriding _get_task_data() func get_interact_data(_from: Node = null) -> Dictionary: if attached_to == null and _from != null: attached_to = _from if attached_to == null: push_error("InteractTask resource trying to be used with no defined node") - return gen_task_data() + return get_task_data() + +func get_task_id() -> int: + return task_id + +func is_task_completed(player_id: int) -> bool: + if not is_player_assigned(player_id): + return false + return get_task_state(player_id) == TaskManager.task_state.COMPLETED + +func get_task_state(player_id: int) -> int: + player_id = normalize_player_id(player_id) + if not is_player_assigned(player_id): + #this player has not been assigned this task + return TaskManager.task_state.INVALID + return task_data_player[player_id]["state"] + +func is_player_assigned(player_id: int) -> bool: + if is_task_global(): + return true + return task_data_player.has(player_id) + +func is_task_global() -> bool: + return task_data["is_task_global"] + +# get the id to assume we are using (assume we are using our own network ID, if the task +# is global use the global ID) +func get_default_id() -> int: + return normalize_player_id(Network.get_my_id()) + +# to get a useable player id (if the task is global and the input is a player id, +# replace it with the global id) +func normalize_player_id(player_id: int): + if is_task_global(): + return TaskManager.GLOBAL_TASK_PLAYER_ID + return player_id + +# to make it easier to store variables in task_data_player +func set_task_data_player_value(key, value, player_id: int = Network.get_my_id()): + if task_registered and is_task_global(): + player_id = TaskManager.GLOBAL_TASK_PLAYER_ID + task_data_player[player_id][key] = value + +func get_task_data_player_value(key, player_id: int = Network.get_my_id()): + if task_registered and is_task_global(): + player_id = TaskManager.GLOBAL_TASK_PLAYER_ID + var data: Dictionary = get_player_task_data(player_id) + if not key in data: + return null + return get_player_task_data(player_id)[key] func _init(): - #print("task init ", task_name) #ensures customizing this resource won't change other resources if Engine.editor_hint: resource_local_to_scene = true diff --git a/src/addons/opensusinteraction/resources/interacttask/interacttask.tres b/src/addons/opensusinteraction/resources/interacttask/interacttask.tres index fa23b9d2..a69905cb 100644 --- a/src/addons/opensusinteraction/resources/interacttask/interacttask.tres +++ b/src/addons/opensusinteraction/resources/interacttask/interacttask.tres @@ -20,8 +20,6 @@ resource_local_to_scene = true resource_name = "InteractTask" script = ExtResource( 1 ) task_text = "" -random_numbers = 0 -task_id = -1 ui_resource = SubResource( 1 ) outputs/toggle_map_interactions = false outputs/output_map_interactions = [ ] diff --git a/src/assets/autoload/helpers.gd b/src/assets/autoload/helpers.gd index b00eb9b5..6576671c 100644 --- a/src/assets/autoload/helpers.gd +++ b/src/assets/autoload/helpers.gd @@ -15,6 +15,13 @@ func string_join(string_array: Array, separator: String) -> String: combined_string += string_array[-1] return combined_string +# merge two dicts together, second dict will overwrite the first if they share a key +func merge_dicts(dict_a: Dictionary, dict_b: Dictionary) -> Dictionary: + var dict: Dictionary = dict_a.duplicate(true) + for key in dict_b: + dict[key] = dict_b[key] + return dict + func find_file(file_name: String, start_path: String = "res://", recursive: bool = true) -> String: """Find file 'file_name' starting at directory 'start_path' recursively.""" var directory := Directory.new() diff --git a/src/assets/autoload/network.gd b/src/assets/autoload/network.gd index d1b0e9fd..00afe2f2 100644 --- a/src/assets/autoload/network.gd +++ b/src/assets/autoload/network.gd @@ -160,7 +160,7 @@ func connect_signals() -> void: get_tree().connect("server_disconnected", self, "_server_disconnected") func get_my_id() -> int: - return myID + return get_tree().get_network_unique_id() func get_player_names() -> Dictionary: return names diff --git a/src/assets/autoload/taskmanager.gd b/src/assets/autoload/taskmanager.gd index 1ab02997..37cbc8d4 100644 --- a/src/assets/autoload/taskmanager.gd +++ b/src/assets/autoload/taskmanager.gd @@ -24,94 +24,154 @@ var task_dict: Dictionary = {} var node_path_resource: Dictionary = {} +const PLAYER_ID_KEY = "player_id" +const TASK_ID_KEY = "task_id" + func _ready(): #warning-ignore:return_value_discarded GameManager.connect("state_changed_priority", self, "_tasks_registered") randomize() self.set_network_master(1) -const PLAYER_ID_KEY = "player_id" -const TASK_ID_KEY = "task_id" - -# GUIs should run this when they think that they have finished the task -func attempt_complete_task(task_info: Dictionary, task_data: Dictionary): +# called when the task is completed +# calls task_completed() on the resource +func task_completed(task_info: Dictionary, data: Dictionary): if not is_task_info_valid(task_info): - push_error("not sending rpc to server; task_info is not valid") + push_error("provided task_info is not valid") assert(false) - return - TaskManager.rpc_id(1, "complete_task_remote", task_info, task_data) + return false -# completes the task on the server, and notifies the client/s that the tasks -# were compleated -master func complete_task_remote(task_info: Dictionary, task_data: Dictionary = {}): + var task_id = task_info[TASK_ID_KEY] + var task_res: InteractTask = get_task_resource(task_id) + if not does_task_exist(task_id): + return + if is_task_global(task_id): + task_info[PLAYER_ID_KEY] = GLOBAL_TASK_PLAYER_ID + if not is_task_completed(task_info): + #warning-ignore:return_value_discarded + task_res.task_completed(task_info[PLAYER_ID_KEY], data) + print("task completed: ", task_id) + emit_signal("task_completed", task_info) + +# RPCed in complete_task() to confirm that the task is actually completed and to sync +master func attempt_complete_task(task_info: Dictionary, task_data: Dictionary): + # should only be run on the server + if not get_tree().is_network_server(): + return if not is_task_info_valid(task_info): - push_error("provided task_info is not valid" + String(task_info)) + push_error("not sending rpc to server; task_info is not valid") assert(false) return - - if not is_valid_rpc_sender(task_info[PLAYER_ID_KEY]): - assert(false) + if is_task_completed(task_info): return - # TODO Probably should validate the task_data + var sender: int = get_tree().get_rpc_sender_id() + var task_id: int = task_info[TASK_ID_KEY] + var player_id: int = task_info[PLAYER_ID_KEY] + var task_res: InteractTask = get_task_resource(task_id) + var global: bool = is_task_global(task_id) + + print("attempting to complete task ", task_id) + + if not global: + if not is_valid_rpc_sender(player_id): + return - # A special case, for when the server's player compleated a task - var completed = false - if task_info[PLAYER_ID_KEY] == 1: - completed = complete_task(task_info, task_data) + var can_complete: bool = task_res.can_complete_task(player_id, task_data) + if can_complete: + # tell everyone that this task was completed + # all task completions should go through confirm_task_completed() at some point, + # this rpc will call it on the server because it is a puppetsync function + # everyone kinda needs to know the task was completed (for task completion bar), + # but we might want to limit how much we tell non-assigned clients about it + # in the future + rpc("confirm_task_completed", task_info, task_data) + else: + # only tell the sender that the task failed to complete because that's the only + # client that needs to know + rpc_id(sender, "deny_task_completed", task_info, task_data) - var task_id = task_info[TASK_ID_KEY] - if is_task_global(task_id): - task_info[PLAYER_ID_KEY] = GLOBAL_TASK_PLAYER_ID - if completed or complete_task(task_info, task_data): - rpc("task_completed", task_info, task_data) - emit_signal("task_completed", task_info) - # If the task is not global, just advance it, so that we know that - # the client has completed it - elif completed or advance_task(task_info, task_state.COMPLETED): - if task_info[PLAYER_ID_KEY] == 1: - emit_signal("task_completed", task_info) - else: - rpc_id(task_info[PLAYER_ID_KEY], "task_completed", task_info, task_data) - -# Called on the client that the task was completed -# or on all the clients if the completed task was global -func complete_task(task_info: Dictionary, data: Dictionary = {}) -> bool: +# called by task resources on the client the task was completed on to notify TaskManager +# that it should confirm the task is actually completed and to sync +func complete_task(task_info: Dictionary, data: Dictionary = {}): if not is_task_info_valid(task_info): - push_error("provided task_info is not valid") + push_error("not sending rpc to server; task_info is not valid") assert(false) - return false + return - # TODO Probably should check that the data has correct values + var task_id: int = task_info[TASK_ID_KEY] + var player_id: int = task_info[PLAYER_ID_KEY] + var task_res: InteractTask = get_task_resource(task_id) + var can_complete: bool = task_res.can_complete_task(player_id, data) - var task_id = task_info[TASK_ID_KEY] - var player_id = task_info[PLAYER_ID_KEY] + if not can_complete: + push_error("not sending rpc to server; task cannot be completed") + return - print("trying to complete task ", task_id) - if not does_task_exist(task_id): - return false - if not advance_task(task_info, task_state.COMPLETED): - return false + rpc_id(1, "attempt_complete_task", task_info, data) - return get_task_resource(task_id).complete_task(player_id, data) +# A callback that the server calls when it successfully completes a task +puppetsync func confirm_task_completed(task_info: Dictionary, data: Dictionary): + if is_task_completed(task_info): + return + task_completed(task_info, data) + +# a callback that the server calls when a client requests to complete a task, but the +# server doesn't agree that it is complete +# warning-ignore:unused_argument +# warning-ignore:unused_argument +puppet func deny_task_completed(task_info: Dictionary, data: Dictionary): + pass + +# used to allow task resources to talk to each other over the network +# RPC modes (puppet, remote, remotesync, etc.) are implemented in the TaskInteract class, +# which provides the same networking security that exists in Node classes +# using remotesync keyword allows each individual task to handle networking their own way, +# this is to avoid limiting task functionality +# this may cause some unexpected behavior when you task_rpc() in an InteractTask script +# because it could change what get_rpc_sender_id() returns, even if the function isn't +# set to sync in the InteractTask script +func task_rset(property: String, value, task_id: int): + var res = get_task_resource(task_id) + if res == null: + return + rpc("receive_task_rset", property, value, task_id) -# A callback that the server calls when it successfully compleats a task -puppet func task_completed(task_info: Dictionary, data: Dictionary): - - if not is_task_info_valid(task_info): - push_error("provided task_info is not valid") - assert(false) - return false - - var task_id = task_info[TASK_ID_KEY] - if not does_task_exist(task_id): +func task_rset_id(id: int, property: String, value, task_id: int): + var res = get_task_resource(task_id) + if res == null: return - if is_task_global(task_id): - task_info[PLAYER_ID_KEY] = GLOBAL_TASK_PLAYER_ID - if not is_task_completed(task_info): - #warning-ignore:return_value_discarded - complete_task(task_info, data) - emit_signal("task_completed", task_info) + rpc_id(id, "receive_task_rset", property, value, task_id) + +# remotesync so it is easier to add sync functionality to task resources +# not puppet so task resources can handle networking their own way +remotesync func receive_task_rset(property: String, value, task_id: int): + var res = get_task_resource(task_id) + if res == null: + return + res.receive_task_rset(property, value) + +# args must be in the form of an array because you can't create functions with variable +# arg amounts in gdscript +func task_rpc(function: String, args: Array, task_id: int): + var res = get_task_resource(task_id) + if res == null: + return + rpc("receive_task_rpc", function, args, task_id) + +func task_rpc_id(id: int, function: String, args: Array, task_id: int): + var res = get_task_resource(task_id) + if res == null: + return + rpc_id(id, "receive_task_rpc", function, args, task_id) + +# remotesync so it is easier to add sync functionality to task resources +# not puppet so task resources can handle networking their own way +remotesync func receive_task_rpc(function: String, args: Array, task_id: int): + var res = get_task_resource(task_id) + if res == null: + return + res.receive_task_rpc(function, args) # Clients run this when they want to populate their GUIs func attempt_request_task_data(task_info: Dictionary): @@ -151,36 +211,20 @@ func networkfy_task_data(task_data: Dictionary) -> Dictionary: filtered.erase(key_to_erase) return filtered - -#can't declare new_state as an int, otherwise it would need to default to an int which could cause later problems -func advance_task(task_info: Dictionary, new_state: int) -> bool: - if not is_task_info_valid(task_info): - return false - var task_id = task_info[TASK_ID_KEY] - var player_id = task_info[PLAYER_ID_KEY] - if not does_task_exist(task_id): - return false - var current_state: int = get_task_resource(task_id).get_task_state(player_id) - #transition if allowed - if task_transitions[current_state].empty(): - #if there are no transitions allowed, ex. a completed task - return false - if typeof(new_state) == TYPE_INT and task_state.values().has(new_state): - #if new_state is a state and the state exists - return transition_task(task_info, new_state) - #transition task to the first transition listed for that task type in task_transitions - return transition_task(task_info, task_transitions[current_state][0]) - -func transition_task(task_info: Dictionary, new_state: int) -> bool: - if not is_task_info_valid(task_info): - return false - var current_state: int = get_task_state(task_info) - #if that task type can't transition from current state to new state - if not task_transitions[current_state].has(new_state): - return false - #transition task - return set_task_state(task_info, new_state) +# DEPRECATED: task resources transition themselves +#func transition_task(task_info: Dictionary, new_state: int) -> bool: +# if not is_task_info_valid(task_info): +# return false +# var current_state: int = get_task_state(task_info) +# #if that task type can't transition from current state to new state +# if not task_transitions[current_state].has(new_state): +# return false +# var task_id = task_info[TASK_ID_KEY] +# var player_id = task_info[PLAYER_ID_KEY] +# #transition task +# return get_task_resource(task_id).transition(new_state, player_id) +# #return set_task_state(task_info, new_state) func register_task(task_resource: Resource): var path = Helpers.get_absolute_path_to(task_resource.attached_to) @@ -189,13 +233,14 @@ func register_task(task_resource: Resource): if not get_tree().is_network_server(): return - var task_id = task_resource.get_task_id() + #var task_id = task_resource.get_task_id() - if task_id == INVALID_TASK_ID: - task_id = gen_unique_id() + #if task_id == INVALID_TASK_ID: + var task_id = gen_unique_id() #node_path_id[path] = task_id var new_task_data: Dictionary = task_resource.get_task_data() + new_task_data[TASK_ID_KEY] = task_id new_task_data["state"] = task_state.NOT_STARTED if not assign_task_data(task_resource, task_id, new_task_data): assert(false) @@ -216,7 +261,7 @@ func _tasks_registered(_old_state, new_state, priority): task["task_data"] = task_resource.get_task_data() registered_tasks.append(task) rpc("assign_task_data_client", registered_tasks) - + puppet func assign_task_data_client(registered_tasks: Array): for task in registered_tasks: var path = task["path"] @@ -267,18 +312,22 @@ func assign_tasks(): print("task assigned,",tasks_to_assign[task]) var tasks_to_send = get_tasks_to_send(id) + # contains additional player specific data to send + var task_info_data_to_send: Dictionary = {} + for task_info in tasks_to_send: + task_info_data_to_send[task_info] = get_player_task_data(task_info) if id == 1: - print("host tasks assigned ", tasks_to_send) + print("host tasks assigned ", task_info_data_to_send) elif not tasks_to_send.empty(): - rpc_id(id,"get_tasks", tasks_to_send) - print("client " + String(id) + " tasks assigned ", tasks_to_send) + rpc_id(id,"receive_tasks", task_info_data_to_send) + print("client " + String(id) + " tasks assigned ", task_info_data_to_send) -puppet func get_tasks(tasks_get: Array): - for task_info in tasks_get: +puppet func receive_tasks(task_info_data: Dictionary): + for task_info in task_info_data.keys(): if is_task_info_valid(task_info): - assign_task(task_info) - print("we got our tasks!") + assign_task(task_info, task_info_data[task_info]) + print("we got our tasks! ", task_info_data) func get_tasks_to_send(player_id: int) -> Array: var arr = [] @@ -294,7 +343,7 @@ func get_player_tasks(player_id: int) -> Array: arr.append(gen_task_info(task_id, player_id)) return arr -func assign_task(task_info: Dictionary) -> void: +func assign_task(task_info: Dictionary, data: Dictionary = {}) -> void: if not is_task_info_valid(task_info): return @@ -307,7 +356,14 @@ func assign_task(task_info: Dictionary) -> void: if not player_tasks[player_id].has(task_id): player_tasks[player_id].append(task_id) #add player_id to assigned_players in task resource - task_dict[task_id].assign_player(player_id) + task_dict[task_id].assign_player(player_id, data) + +func can_complete_task(task_info) -> bool: + if not is_task_info_valid(task_info): + return false + var task_id: int = task_info[TASK_ID_KEY] + var task_res: Resource = get_task_resource(task_id) + return task_res.can_complete_task() func get_task_data(task_info: Dictionary) -> Dictionary: if not is_task_info_valid(task_info): @@ -316,17 +372,17 @@ func get_task_data(task_info: Dictionary) -> Dictionary: var player_id = task_info[PLAYER_ID_KEY] return get_task_resource(task_id).get_task_data(player_id) -func get_task_resource(task_id: int) -> Resource: - if not does_task_exist(task_id): - return null - return task_dict[task_id] - -func set_task_state(task_info: Dictionary, new_state: int) -> bool: +func get_player_task_data(task_info: Dictionary) -> Dictionary: if not is_task_info_valid(task_info): - return false + return {} var task_id = task_info[TASK_ID_KEY] var player_id = task_info[PLAYER_ID_KEY] - return get_task_resource(task_id).set_task_state(player_id, new_state) + return get_task_resource(task_id).get_player_task_data(player_id) + +func get_task_resource(task_id: int) -> InteractTask: + if not does_task_exist(task_id): + return null + return task_dict[task_id] func get_task_state(task_info: Dictionary) -> int: if not is_task_info_valid(task_info): diff --git a/src/assets/common/classes/ui/task/controltask.gd b/src/assets/common/classes/ui/task/controltask.gd index b80a029e..81d236e2 100644 --- a/src/assets/common/classes/ui/task/controltask.gd +++ b/src/assets/common/classes/ui/task/controltask.gd @@ -8,9 +8,9 @@ func _ready(): #warning-ignore:return_value_discarded TaskManager.connect("receive_task_data", self, "_on_received_task_data") -func complete_task(data: Dictionary = {}): - var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) - TaskManager.attempt_complete_task(taskInfo, data) +#func complete_task(data: Dictionary = {}): +# var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) +# TaskManager.attempt_complete_task(taskInfo, data) func _on_task_completed(taskInfo: Dictionary): if not TaskManager.is_task_info_valid(taskInfo): diff --git a/src/assets/common/classes/ui/task/popuppaneltask.gd b/src/assets/common/classes/ui/task/popuppaneltask.gd index 3b5979e2..0800323c 100644 --- a/src/assets/common/classes/ui/task/popuppaneltask.gd +++ b/src/assets/common/classes/ui/task/popuppaneltask.gd @@ -8,9 +8,9 @@ func _ready(): #warning-ignore:return_value_discarded TaskManager.connect("receive_task_data", self, "_on_received_task_data") -func complete_task(data: Dictionary = {}): - var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) - TaskManager.attempt_complete_task(taskInfo, data) +#func complete_task(data: Dictionary = {}): +# var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) +# TaskManager.attempt_complete_task(taskInfo, data) func _on_task_completed(taskInfo: Dictionary): if not TaskManager.is_task_info_valid(taskInfo): diff --git a/src/assets/common/classes/ui/task/popuptask.gd b/src/assets/common/classes/ui/task/popuptask.gd index b8a8f006..22431633 100644 --- a/src/assets/common/classes/ui/task/popuptask.gd +++ b/src/assets/common/classes/ui/task/popuptask.gd @@ -8,9 +8,9 @@ func _ready(): #warning-ignore:return_value_discarded TaskManager.connect("receive_task_data", self, "_on_received_task_data") -func complete_task(data: Dictionary = {}): - var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) - TaskManager.attempt_complete_task(taskInfo, data) +#func complete_task(data: Dictionary = {}): +# var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) +# TaskManager.attempt_complete_task(taskInfo, data) func _on_task_completed(taskInfo: Dictionary): if not TaskManager.is_task_info_valid(taskInfo): diff --git a/src/assets/common/classes/ui/task/windowdialogtask.gd b/src/assets/common/classes/ui/task/windowdialogtask.gd index cea95605..da038996 100644 --- a/src/assets/common/classes/ui/task/windowdialogtask.gd +++ b/src/assets/common/classes/ui/task/windowdialogtask.gd @@ -8,9 +8,9 @@ func _ready(): #warning-ignore:return_value_discarded TaskManager.connect("receive_task_data", self, "_on_received_task_data") -func complete_task(data: Dictionary = {}): - var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) - TaskManager.attempt_complete_task(taskInfo, data) +#func complete_task(data: Dictionary = {}): +# var taskInfo = TaskManager.gen_task_info(ui_data["task_id"]) +# TaskManager.attempt_complete_task(taskInfo, data) func _on_task_completed(taskInfo: Dictionary): if not TaskManager.is_task_info_valid(taskInfo): diff --git a/src/assets/maps/test/clocktexttemp.gd b/src/assets/maps/test/clocktexttemp.gd index 2f6d9151..4cad9b13 100644 --- a/src/assets/maps/test/clocktexttemp.gd +++ b/src/assets/maps/test/clocktexttemp.gd @@ -12,6 +12,3 @@ func interacted_with(interactNode, from, interact_data): if interact_data.has("newText"): text = interact_data["newText"] return - if PlayerManager.assignedtasks[0] == 0: - interact_resource.interact(self, {"linkedNode": self, "currentTime": int(text)}) -#test if client has correct assigned task diff --git a/src/assets/maps/test/tasks/clockset/interactpointclockset.tres b/src/assets/maps/test/tasks/clockset/interactpointclockset.tres new file mode 100644 index 00000000..594a108f --- /dev/null +++ b/src/assets/maps/test/tasks/clockset/interactpointclockset.tres @@ -0,0 +1,35 @@ +[gd_resource type="Resource" load_steps=6 format=2] + +[ext_resource path="res://assets/ui/tasks/clockset/clocksetinteracttask.gd" type="Script" id=1] +[ext_resource path="res://addons/opensusinteraction/resources/interactui/interactui.gd" type="Script" id=2] +[ext_resource path="res://addons/opensusinteraction/resources/interactmap/interactmap.gd" type="Script" id=3] + +[sub_resource type="Resource" id=1] +resource_local_to_scene = true +resource_name = "InteractMap" +script = ExtResource( 3 ) +interact_with = NodePath("Label") +interact_data = { + +} +advanced/network_sync = false + +[sub_resource type="Resource" id=2] +resource_local_to_scene = true +script = ExtResource( 2 ) +ui_name = "clockset" +ui_data = { + +} +action = 0 +advanced/reinstance = false +advanced/free_on_close = false + +[resource] +resource_local_to_scene = true +script = ExtResource( 1 ) +task_text = "interactpoint clockset task" +ui_resource = SubResource( 2 ) +outputs/toggle_map_interactions = true +outputs/output_map_interactions = [ SubResource( 1 ) ] +is_task_global = false diff --git a/src/assets/maps/test/tasks/clockset/standbuttonclockset.tres b/src/assets/maps/test/tasks/clockset/standbuttonclockset.tres new file mode 100644 index 00000000..2b4e47ed --- /dev/null +++ b/src/assets/maps/test/tasks/clockset/standbuttonclockset.tres @@ -0,0 +1,35 @@ +[gd_resource type="Resource" load_steps=6 format=2] + +[ext_resource path="res://assets/ui/tasks/clockset/clocksetinteracttask.gd" type="Script" id=1] +[ext_resource path="res://addons/opensusinteraction/resources/interactui/interactui.gd" type="Script" id=2] +[ext_resource path="res://addons/opensusinteraction/resources/interactmap/interactmap.gd" type="Script" id=3] + +[sub_resource type="Resource" id=1] +resource_local_to_scene = true +resource_name = "InteractMap" +script = ExtResource( 3 ) +interact_with = NodePath("clocktext") +interact_data = { + +} +advanced/network_sync = false + +[sub_resource type="Resource" id=2] +resource_local_to_scene = true +script = ExtResource( 2 ) +ui_name = "clockset" +ui_data = { + +} +action = 0 +advanced/reinstance = false +advanced/free_on_close = false + +[resource] +resource_local_to_scene = true +script = ExtResource( 1 ) +task_text = "standbutton clockset task" +ui_resource = SubResource( 2 ) +outputs/toggle_map_interactions = true +outputs/output_map_interactions = [ SubResource( 1 ) ] +is_task_global = true diff --git a/src/assets/maps/test/test.tscn b/src/assets/maps/test/test.tscn index 5b8089b8..0b5a5616 100644 --- a/src/assets/maps/test/test.tscn +++ b/src/assets/maps/test/test.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=38 format=2] +[gd_scene load_steps=28 format=2] [ext_resource path="res://assets/common/textures/icons/spawnpad.png" type="Texture" id=1] [ext_resource path="res://assets/maps/common/dynamic/testdoor/testdoor.tscn" type="PackedScene" id=2] @@ -11,9 +11,11 @@ [ext_resource path="res://addons/opensusinteraction/resources/interact/interact.gd" type="Script" id=9] [ext_resource path="res://addons/opensusinteraction/resources/interactmap/interactmap.gd" type="Script" id=10] [ext_resource path="res://addons/opensusinteraction/resources/interactui/interactui.gd" type="Script" id=11] +[ext_resource path="res://assets/maps/test/tasks/clockset/standbuttonclockset.tres" type="Resource" id=12] [ext_resource path="res://assets/items/battery.tscn" type="PackedScene" id=13] [ext_resource path="res://assets/items/wrench.tscn" type="PackedScene" id=14] [ext_resource path="res://assets/maps/map.gd" type="Script" id=15] +[ext_resource path="res://assets/maps/test/tasks/clockset/interactpointclockset.tres" type="Resource" id=16] [sub_resource type="Resource" id=1] resource_local_to_scene = true @@ -25,7 +27,7 @@ interact_data = { } advanced/network_sync = false -[sub_resource type="Resource" id=2] +[sub_resource type="Resource" id=12] resource_local_to_scene = true resource_name = "InteractUI" script = ExtResource( 11 ) @@ -33,18 +35,18 @@ ui_name = "" ui_data = { } +action = 0 advanced/reinstance = false -advanced/only_instance = false +advanced/free_on_close = false [sub_resource type="Resource" id=3] resource_local_to_scene = true resource_name = "InteractTask" script = ExtResource( 8 ) task_text = "" -random_numbers = 0 -task_id = -1 -ui_resource = SubResource( 2 ) +ui_resource = SubResource( 12 ) outputs/toggle_map_interactions = false +outputs/output_map_interactions = [ ] is_task_global = false [sub_resource type="Resource" id=4] @@ -55,8 +57,9 @@ ui_name = "" ui_data = { } +action = 0 advanced/reinstance = false -advanced/only_instance = false +advanced/free_on_close = false [sub_resource type="Resource" id=5] resource_local_to_scene = true @@ -69,74 +72,16 @@ map_resource = SubResource( 1 ) [sub_resource type="Resource" id=6] resource_local_to_scene = true -resource_name = "InteractMap" -script = ExtResource( 10 ) -interact_with = NodePath("clocktext") -interact_data = { - -} -advanced/network_sync = false - -[sub_resource type="Resource" id=7] -resource_local_to_scene = true -resource_name = "InteractMap" -script = ExtResource( 10 ) -interact_with = NodePath("clocktext") -interact_data = { - -} -advanced/network_sync = false - -[sub_resource type="Resource" id=8] -resource_local_to_scene = true -script = ExtResource( 11 ) -ui_name = "clockset" -ui_data = { - -} -advanced/reinstance = false -advanced/only_instance = false - -[sub_resource type="Resource" id=9] -resource_local_to_scene = true -script = ExtResource( 8 ) -task_text = "Task system clockset standbutton" -random_numbers = 2 -task_id = -1 -ui_resource = SubResource( 8 ) -outputs/toggle_map_interactions = true -outputs/output_map_interactions = [ SubResource( 7 ) ] -is_task_global = true - -[sub_resource type="Resource" id=10] -resource_local_to_scene = true -script = ExtResource( 11 ) -ui_name = "" -ui_data = { - -} -advanced/reinstance = false -advanced/only_instance = false - -[sub_resource type="Resource" id=11] -resource_local_to_scene = true -script = ExtResource( 9 ) -interact_type = 0 -task_resource = SubResource( 9 ) -ui_resource = SubResource( 10 ) -map_resource = SubResource( 6 ) - -[sub_resource type="Resource" id=12] -resource_local_to_scene = true script = ExtResource( 11 ) ui_name = "clockset" ui_data = { } +action = 0 advanced/reinstance = false -advanced/only_instance = false +advanced/free_on_close = false -[sub_resource type="Resource" id=13] +[sub_resource type="Resource" id=7] resource_local_to_scene = true script = ExtResource( 10 ) interact_with = NodePath("") @@ -145,101 +90,45 @@ interact_data = { } advanced/network_sync = false -[sub_resource type="Resource" id=14] +[sub_resource type="Resource" id=13] resource_local_to_scene = true +resource_name = "InteractUI" script = ExtResource( 11 ) ui_name = "" ui_data = { } +action = 0 advanced/reinstance = false -advanced/only_instance = false +advanced/free_on_close = false -[sub_resource type="Resource" id=15] +[sub_resource type="Resource" id=9] resource_local_to_scene = true script = ExtResource( 8 ) task_text = "" -random_numbers = 0 -task_id = -1 -ui_resource = SubResource( 14 ) +ui_resource = SubResource( 13 ) outputs/toggle_map_interactions = false +outputs/output_map_interactions = [ ] is_task_global = false -[sub_resource type="Resource" id=16] +[sub_resource type="Resource" id=10] resource_local_to_scene = true script = ExtResource( 11 ) ui_name = "chatbox" ui_data = { } +action = 0 advanced/reinstance = false -advanced/only_instance = false +advanced/free_on_close = false -[sub_resource type="Resource" id=17] +[sub_resource type="Resource" id=11] resource_local_to_scene = true script = ExtResource( 9 ) interact_type = 1 -task_resource = SubResource( 15 ) -ui_resource = SubResource( 16 ) -map_resource = SubResource( 13 ) - -[sub_resource type="Resource" id=18] -resource_local_to_scene = true -script = ExtResource( 10 ) -interact_with = NodePath("") -interact_data = { - -} -advanced/network_sync = false - -[sub_resource type="Resource" id=19] -resource_local_to_scene = true -resource_name = "InteractMap" -script = ExtResource( 10 ) -interact_with = NodePath("Label") -interact_data = { - -} -advanced/network_sync = false - -[sub_resource type="Resource" id=20] -resource_local_to_scene = true -script = ExtResource( 11 ) -ui_name = "clockset" -ui_data = { - -} -advanced/reinstance = false -advanced/only_instance = false - -[sub_resource type="Resource" id=21] -resource_local_to_scene = true -script = ExtResource( 8 ) -task_text = "Task system clockset" -random_numbers = 2 -task_id = -1 -ui_resource = SubResource( 20 ) -outputs/toggle_map_interactions = true -outputs/output_map_interactions = [ SubResource( 19 ) ] -is_task_global = false - -[sub_resource type="Resource" id=22] -resource_local_to_scene = true -script = ExtResource( 11 ) -ui_name = "" -ui_data = { - -} -advanced/reinstance = false -advanced/only_instance = false - -[sub_resource type="Resource" id=23] -resource_local_to_scene = true -script = ExtResource( 9 ) -interact_type = 0 -task_resource = SubResource( 21 ) -ui_resource = SubResource( 22 ) -map_resource = SubResource( 18 ) +task_resource = SubResource( 9 ) +ui_resource = SubResource( 10 ) +map_resource = SubResource( 7 ) [node name="test" type="YSort"] script = ExtResource( 15 ) @@ -262,7 +151,7 @@ interact_on_exit = true [node name="standbutton2" parent="." instance=ExtResource( 3 )] position = Vector2( 0, -350 ) -interact_resource = SubResource( 11 ) +interact_resource = ExtResource( 12 ) only_main_player = true [node name="clocktext2" type="Label" parent="standbutton2"] @@ -288,11 +177,11 @@ script = ExtResource( 4 ) __meta__ = { "_edit_use_anchors_": false } -interact_resource = SubResource( 12 ) +interact_resource = SubResource( 6 ) [node name="interactpoint" parent="." instance=ExtResource( 5 )] position = Vector2( -450, 0 ) -interact_resource = SubResource( 17 ) +interact_resource = SubResource( 11 ) display_text = "Interact chat" [node name="Polygon2D" type="Polygon2D" parent="interactpoint"] @@ -326,7 +215,7 @@ __meta__ = { [node name="tasktest" parent="." instance=ExtResource( 5 )] position = Vector2( 450, 0 ) -interact_resource = SubResource( 23 ) +interact_resource = ExtResource( 16 ) display_text = "task test" [node name="Label" type="Label" parent="tasktest"] diff --git a/src/assets/ui/hud/defaulthud/taskinfo/taskinfo.gd b/src/assets/ui/hud/defaulthud/taskinfo/taskinfo.gd index 38b941af..9e402d72 100644 --- a/src/assets/ui/hud/defaulthud/taskinfo/taskinfo.gd +++ b/src/assets/ui/hud/defaulthud/taskinfo/taskinfo.gd @@ -27,7 +27,8 @@ func _on_task_completed(task_info: Dictionary): var playerID = task_info[TaskManager.PLAYER_ID_KEY] if not tasks.has(playerID): - assert(false) + # removing this assert because it will fire when the game is functioning correctly + #assert(false) return var allTasksCompleted = true diff --git a/src/assets/ui/tasks/clockset/clockset.gd b/src/assets/ui/tasks/clockset/clockset.gd index b95123a6..23a23e86 100644 --- a/src/assets/ui/tasks/clockset/clockset.gd +++ b/src/assets/ui/tasks/clockset/clockset.gd @@ -1,9 +1,5 @@ extends WindowDialogTask -#var ui_data: Dictionary = {} -var targetTime: int = 433 -var currentTime: int = 630 - onready var hoursNode: Node = get_node("clock/hours") onready var minutesNode: Node = get_node("clock/minutes") onready var ampmNode: Node = get_node("clock/ampm") @@ -14,30 +10,36 @@ func _ready(): ampmNode.get_line_edit().connect("focus_entered", self, "_on_ampm_focus_entered") func open(): -# warning-ignore:narrowing_conversion - + var res: Resource = get_res() + if not res.is_connected("times_updated", self, "times_updated"): +# warning-ignore:return_value_discarded + res.connect("times_updated", self, "times_updated") ui_data_updated() - func ui_data_updated(): - if ui_data.has("task_data") and ui_data["task_data"] is Array: - if ui_data["task_data"].size() > 0: - targetTime = normalise_time(ui_data["task_data"][0]) - if ui_data["task_data"].size() > 1: - currentTime = normalise_time(ui_data["task_data"][1]) - - setClockTime(currentTime) - setWatchTime(targetTime) - + setClockTime(getCurrentTime()) + setWatchTime(getTargetTime()) + checkComplete() + func checkComplete(): updateCurrentTime() - if currentTime == targetTime: - taskComplete() + var completed: bool = is_task_completed() + if completed: + get_res().complete_task() + set_clock_editable(not completed) + +#func taskComplete(): +# .complete_task({"newText": str(getCurrentTime())}) + +func sync_task(): + get_res().sync_task() -func taskComplete(): - .complete_task({"newText": str(currentTime)}) +func set_clock_editable(editable: bool): + for node in [hoursNode, minutesNode, ampmNode]: + node.editable = editable func setClockTime(newTime: int): +# warning-ignore:integer_division hoursNode.value = roundDown(newTime / 100, 1) minutesNode.value = newTime % 100 @@ -45,7 +47,17 @@ func setWatchTime(newTime): $watch/watchface.showTime(newTime) func updateCurrentTime(): - currentTime = (hoursNode.value * 100) + minutesNode.value + var time = (hoursNode.value * 100) + minutesNode.value + get_res().set_current_time(time) + +func getTargetTime() -> int: + return get_res().get_target_time() + +func getCurrentTime() -> int: + return get_res().get_current_time() + +func get_res() -> Resource: + return TaskManager.get_task_resource(ui_data[TaskManager.TASK_ID_KEY]) func _on_hours_value_changed(value): if value == 0: @@ -100,6 +112,16 @@ func roundDown(num, step) -> int: return normRound - step return int(normRound) +func times_updated(_target: int, _current: int, task_res: Resource): + # if the current task data matches the resource this signal is from + # this should prevent weirdness if multiple tasks are using this ui + if task_res != get_res(): + return + ui_data_updated() + +func is_task_completed() -> bool: + return get_res().can_complete_task() + #so you can't type into the spinboxes func _on_hours_focus_entered(): grab_focus() @@ -109,3 +131,6 @@ func _on_minutes_focus_entered(): func _on_ampm_focus_entered(): grab_focus() + +func _on_clockset_popup_hide(): + sync_task() diff --git a/src/assets/ui/tasks/clockset/clockset.tscn b/src/assets/ui/tasks/clockset/clockset.tscn index 63ad1294..4759562b 100644 --- a/src/assets/ui/tasks/clockset/clockset.tscn +++ b/src/assets/ui/tasks/clockset/clockset.tscn @@ -273,6 +273,7 @@ valign = 1 __meta__ = { "_edit_use_anchors_": false } +[connection signal="popup_hide" from="." to="." method="_on_clockset_popup_hide"] [connection signal="focus_entered" from="clock/hours" to="." method="_on_hours_focus_entered"] [connection signal="value_changed" from="clock/hours" to="." method="_on_hours_value_changed"] [connection signal="focus_entered" from="clock/minutes" to="." method="_on_minutes_focus_entered"] diff --git a/src/assets/ui/tasks/clockset/clocksetinteracttask.gd b/src/assets/ui/tasks/clockset/clocksetinteracttask.gd new file mode 100644 index 00000000..31b73d51 --- /dev/null +++ b/src/assets/ui/tasks/clockset/clocksetinteracttask.gd @@ -0,0 +1,113 @@ +tool +extends InteractTask + +signal times_updated(target, current, task_res) + +func _init(): + add_networked_func("receive_times", MultiplayerAPI.RPC_MODE_REMOTE) + +# warning-ignore:unused_argument +func _complete_task(player_id: int, _data: Dictionary): + sync_task() + +# warning-ignore:unused_argument +# warning-ignore:unused_argument +func _can_complete_task(player_id: int, data: Dictionary): + return get_target_time(player_id) == get_current_time(player_id) + +func _assign_player(player_id: int, data: Dictionary): + if "target_time" in data: + set_target_time(data["target_time"]) + if "current_time" in data: + set_current_time(data["current_time"]) + update_map_text(player_id) + +func _sync_task(): + send_times(get_target_time(), get_current_time()) + +#func _init_resource(_from: Node): +# if not get_tree().is_network_server(): +# return +# target_time = gen_rand_time() +# current_time = gen_rand_time() +# emit_signal("times_updated", target_time, current_time, self) + +# warning-ignore:unused_argument +func _get_task_data(player_id: int) -> Dictionary: + var dict: Dictionary = {} + dict["newText"] = str(get_current_time()) + return dict + +func _gen_player_task_data(_player_id: int) -> Dictionary: + var data: Dictionary = {} + if get_tree().is_network_server(): + data["target_time"] = gen_rand_time() + data["current_time"] = gen_rand_time() + return data + +func _registered(_new_task_id: int, new_task_data: Dictionary): + # calling is_network_server() on task manager because the function does not + # exist in resources + if TaskManager.get_tree().is_network_server(): + return + for property in ["target_time", "current_time"]: + if new_task_data.has(property): + set(property, new_task_data[property]) + +# keeping target as an input to avoid changing the code, the only time target_time is +# updated is in _assign_player() +func send_times(target: int, current: int, player_id: int = Network.get_my_id()): + if task_registered and is_task_global(): + player_id = TaskManager.GLOBAL_TASK_PLAYER_ID + #print("sending times out to network") + task_rpc("receive_times", [target, current, player_id]) + update_map_text(player_id) + +func receive_times(target: int, current: int, player_id: int): + #print("received times, target: ", target, " current: ", current) + # target time shouldn't change + # set_target_time(target, player_id) + set_current_time(current, player_id) + emit_signal("times_updated", get_target_time(), get_current_time(), self) + update_map_text(player_id) + +# trigger every InteractMap resource connected to this task +# used to update map text without finishing the task +func update_map_text(player_id: int): + if not is_player_assigned(Network.get_my_id()): + return + var temp_interact_data = get_task_data(player_id) + if map_outputs_on: + for resource in map_outputs: + resource.interact(attached_to, temp_interact_data) + +func set_target_time(time: int, player_id: int = Network.get_my_id()): + set_task_data_player_value("target_time", time, player_id) + +func get_target_time(player_id: int = Network.get_my_id()) -> int: + return get_task_data_player_value("target_time", player_id) + +func set_current_time(time: int, player_id: int = Network.get_my_id()): + set_task_data_player_value("current_time", time, player_id) + +func get_current_time(player_id: int = Network.get_my_id()) -> int: + return get_task_data_player_value("current_time", player_id) + +func gen_rand_time() -> int: + return normalise_time(randi()) + +# returns a valid time(from 00:00 to 12:59) +# num can be any value +func normalise_time(num: int) -> int: + num = num % 1300 + num = roundDown(num, 100) + (num % 100) % 60 + if num < 100: + # this is military time, so can't have values smaller than 100 + num += 1200 + return num + +func roundDown(num, step) -> int: + var normRound = stepify(num, step) + if normRound > num: + return normRound - step + return int(normRound) diff --git a/src/assets/ui/tasks/clockset/clocksetinteracttask.tres b/src/assets/ui/tasks/clockset/clocksetinteracttask.tres new file mode 100644 index 00000000..804f4d20 --- /dev/null +++ b/src/assets/ui/tasks/clockset/clocksetinteracttask.tres @@ -0,0 +1,25 @@ +[gd_resource type="Resource" load_steps=4 format=2] + +[ext_resource path="res://assets/ui/tasks/clockset/clocksetinteracttask.gd" type="Script" id=1] +[ext_resource path="res://addons/opensusinteraction/resources/interactui/interactui.gd" type="Script" id=2] + +[sub_resource type="Resource" id=1] +resource_local_to_scene = true +resource_name = "InteractUI" +script = ExtResource( 2 ) +ui_name = "clockset" +ui_data = { + +} +action = 0 +advanced/reinstance = false +advanced/free_on_close = false + +[resource] +resource_local_to_scene = true +script = ExtResource( 1 ) +task_text = "clockset task" +ui_resource = SubResource( 1 ) +outputs/toggle_map_interactions = true +outputs/output_map_interactions = [ ] +is_task_global = true diff --git a/src/project.godot b/src/project.godot index 6abeb4a8..81d0845c 100644 --- a/src/project.godot +++ b/src/project.godot @@ -29,6 +29,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://assets/common/classes/objectpool/ipoolable.gd" }, { +"base": "Resource", +"class": "InteractTask", +"language": "GDScript", +"path": "res://addons/opensusinteraction/resources/interacttask/interacttask.gd" +}, { "base": "KinematicBody2D", "class": "Item", "language": "GDScript", @@ -89,6 +94,7 @@ _global_script_class_icons={ "ControlBase": "", "ControlTask": "", "IPoolable": "", +"InteractTask": "", "Item": "", "ItemHandler": "", "MapInfo": "",