feat: This introduces background events queue in Escoria (#444)

Co-authored-by: Dennis Ploeger <develop@dieploegers.de>
This commit is contained in:
Dennis Ploeger
2021-11-22 11:04:35 +01:00
committed by GitHub
parent f7c0bf2224
commit 9adc7bbade
12 changed files with 320 additions and 113 deletions

View File

@@ -61,7 +61,7 @@ func run(command_params: Array) -> int:
var exited_previous_room = false
if command_params[1] \
and escoria.event_manager._running_event.name \
and escoria.event_manager.get_running_event("_front").name \
in ["exit_scene", "room_selector"]:
exited_previous_room = true
escoria.main.scene_transition.transition(
@@ -111,7 +111,7 @@ func run(command_params: Array) -> int:
var room_scene = res_room.instance()
if room_scene:
if command_params[1] \
and escoria.event_manager._running_event.name \
and escoria.event_manager.get_running_event("_front").name \
== "room_selector":
room_scene.enabled_automatic_transitions = true
else:

View File

@@ -0,0 +1,99 @@
# `queue_event object event [channel] [block]`
#
# Queue another event to run
#
# **Parameters**
#
# - object: Object that holds the ESC script with the event
# - event: Name of the event to queue
# - channel: Channel to run the event on (default: `_front`)
# - block: Whether to wait for the queue to finish. This is only possible, if
# the queued event is not to be run on the same event as this command
# (default: `false`)
#
# @ESC
extends ESCBaseCommand
class_name QueueEventCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_STRING, TYPE_BOOL],
[null, null, "_front", false]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"queue_event.gd:validate",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
var node = escoria.object_manager.objects.get(
arguments[0]
).node
if not "esc_script" in node or node.esc_script == "":
escoria.logger.report_errors(
"queue_event.gd:validate",
[
"Object with global id %s has no ESC script" % arguments[0]
]
)
return false
var esc_script = escoria.esc_compiler.load_esc_file(node.esc_script)
if not arguments[1] in esc_script.events:
escoria.logger.report_errors(
"queue_event.gd:validate",
[
"Event with name %s not found" % arguments[1]
]
)
return false
if arguments[3] and not escoria.event_manager.is_channel_free(arguments[2]):
escoria.logger.report_errors(
"queue_event.gd:validate",
[
"The queue %s doesn't accept a new event." % arguments[2]
]
)
return false
return .validate(arguments)
# Run the command
func run(arguments: Array) -> int:
var node = escoria.object_manager.objects.get(
arguments[0]
).node
var esc_script = escoria.esc_compiler.load_esc_file(node.esc_script)
if arguments[2] == "_front":
escoria.event_manager.queue_event(esc_script.events[arguments[1]])
else:
escoria.event_manager.queue_background_event(
arguments[2],
esc_script.events[arguments[1]]
)
if arguments[3]:
if arguments[2] == "_front":
var rc = yield(escoria.event_manager, "event_finished")
while rc[1] != arguments[1]:
rc = yield(escoria.event_manager, "event_finished")
return rc
else:
var rc = yield(
escoria.event_manager,
"background_event_finished"
)
while rc[1] != arguments[1] and rc[2] != arguments[2]:
rc = yield(
escoria.event_manager,
"background_event_finished"
)
return rc
return ESCExecution.RC_OK

View File

@@ -1,106 +1,194 @@
# A manager for running events
# There are different "channels" an event can run on.
# The usual events happen in the foreground channel _front, but
# additional event queues can be added as required.
# Additionally, events can be scheduled to be queued in the future
extends Node
class_name ESCEventManager
# Emitted when the event started execution
signal event_started(event_name)
# Emitted when an event is started in a channel of the background queue
signal background_event_started(channel_name, event_name)
# Emitted when the event did finish running
signal event_finished(event_name, return_code)
signal event_finished(return_code, event_name)
# Emitted when the event was interrupted
signal event_interrupted(event_name, return_code)
# Emitted when a background event was finished
signal background_event_finished(return_code, event_name, channel_name)
# A queue of events to run
var events_queue: Array = []
# A list of currently scheduled events
var scheduled_events: Array = []
# Currently running event
var _running_event: ESCEvent
# A list of constantly running events in multiple background channels
var events_queue: Dictionary = {
"_front": []
}
# Whether the event manager is allowed to proceed with next event.
var can_process_next_event = true
# Currently running event in background channels
var _running_events: Dictionary = {}
# Wether an event can be played on a specific channel
var _channels_state: Dictionary = {}
# Make sure to stop when pausing the game
func _ready():
self.pause_mode = Node.PAUSE_MODE_STOP
# Handle the events queue and scheduled events
#
# #### Parameters
# - delta: Time passed since the last process call
func _process(delta: float) -> void:
if events_queue.size() > 0 and can_process_next_event:
can_process_next_event = false
_running_event = events_queue.pop_front()
escoria.logger.debug(
"esc_event_manager",
[
"Popping event %s from event_queue" \
% _running_event.name if _running_event.get("name") != null \
else str(_running_event)
]
)
if not _running_event.is_connected(
"finished", self, "_on_event_finished"
):
_running_event.connect(
"finished",
self,
"_on_event_finished",
[_running_event]
for channel_name in events_queue.keys():
if events_queue[channel_name].size() == 0:
continue
if is_channel_free(channel_name):
_channels_state[channel_name] = false
_running_events[channel_name] = \
events_queue[channel_name].pop_front()
escoria.logger.debug(
"esc_event_manager",
[
"Popping event %s from background queue %s" % [
_running_events[channel_name].name,
channel_name
]
]
)
if not _running_event.is_connected(
"interrupted", self, "_on_event_finished"
):
_running_event.connect(
"interrupted",
self,
"_on_event_finished",
[_running_event]
)
emit_signal("event_started", _running_event.name)
_running_event.run()
if not _running_events[channel_name].is_connected(
"finished", self, "_on_event_finished"
):
_running_events[channel_name].connect(
"finished",
self,
"_on_event_finished",
[channel_name]
)
if not _running_events[channel_name].is_connected(
"interrupted", self, "_on_event_finished"
):
_running_events[channel_name].connect(
"interrupted",
self,
"_on_event_finished",
[channel_name]
)
if channel_name == "_front":
emit_signal(
"event_started",
_running_events[channel_name].name
)
else:
emit_signal(
"background_event_started",
channel_name,
_running_events[channel_name].name
)
_running_events[channel_name].run()
for event in self.scheduled_events:
(event as ESCScheduledEvent).timeout -= delta
if (event as ESCScheduledEvent).timeout <= 0:
self.scheduled_events.erase(event)
self.events_queue.append(event.event)
self.events_queue['_front'].append(event.event)
# Queue a new event to run
# Queue a new event to run in the foreground
#
# #### Parameters
# - event: Event to run
func queue_event(event: ESCEvent) -> void:
events_queue.append(event)
self.events_queue['_front'].append(event)
# Schedule an event to run after a timeout
#
# #### Parameters
# - event: Event to run
# - timeout: Number of seconds to wait before adding the event to the
# front queue
func schedule_event(event: ESCEvent, timeout: float) -> void:
scheduled_events.append(ESCScheduledEvent.new(event, timeout))
# Queue the run of an event in a background channel
#
# #### Parameters
# - channel_name: Name of the channel to use
# - event: Event to run
func queue_background_event(channel_name: String, event: ESCEvent) -> void:
if not channel_name in events_queue:
events_queue[channel_name] = []
events_queue[channel_name].append(event)
# Interrupt the events currently running.
func interrupt_running_event():
for channel_name in _running_events.keys():
if _running_events[channel_name] != null:
_running_events[channel_name].interrupt()
# Clears the event queues.
func clear_event_queue():
for channel_name in events_queue.keys():
events_queue[channel_name].clear()
# Check wether a channel is free to run more events
#
# #### Parameters
# - name: Name of the channel to test
# **Returns** Wether the channel can currently accept a new event
func is_channel_free(name: String) -> bool:
return _channels_state[name] if name in _channels_state else true
# Get the currently running event in a channel
#
# #### Parameters
# - name: Name of the channel
# **Returns** The currently running event or null
func get_running_event(name: String) -> ESCEvent:
return _running_events[name] if name in _running_events else null
# The event finished running
func _on_event_finished(return_code: int, event: ESCEvent) -> void:
#
# #### Parameters
# - return_code: Return code of the finished event
# - channel_name: Name of the channel that the event came from
func _on_event_finished(return_code: int, channel_name: String) -> void:
var event = _running_events[channel_name]
escoria.logger.debug(
"Event %s ended with return code %d" % [event.name, return_code]
)
event.disconnect("finished", self, "_on_event_finished")
event.disconnect("interrupted", self, "_on_event_finished")
_running_event = null
can_process_next_event = true
match(return_code):
ESCExecution.RC_CANCEL:
return_code = ESCExecution.RC_OK
emit_signal("event_finished", return_code, event.name)
if return_code == ESCExecution.RC_CANCEL:
return_code = ESCExecution.RC_OK
# Interrupt the event currently running.
func interrupt_running_event():
if _running_event == null:
return
_running_event.interrupt()
# Clears the event queue.
func clear_event_queue():
events_queue.clear()
_running_events[channel_name] = null
_channels_state[channel_name] = true
if channel_name == "_front":
emit_signal(
"event_finished",
return_code,
event.name
)
else:
emit_signal(
"background_event_finished",
return_code,
event.name,
channel_name
)

View File

@@ -37,7 +37,8 @@ func prepare_arguments(arguments: Array) -> Array:
for index in range(arguments.size()):
complete_arguments[index] = escoria.utils.get_typed_value(
arguments[index]
arguments[index],
types[index]
)
var strip = strip_quotes[0]
if strip_quotes.size() == complete_arguments.size():

View File

@@ -118,9 +118,13 @@ func _ready():
# Performs the ESC script events "setup" and "ready", in this order, if they are
# present. Also manages automatic transitions.
func perform_script_events():
if esc_script and escoria.event_manager._running_event == null \
or (escoria.event_manager._running_event != null \
and escoria.event_manager._running_event.name != "load"):
if esc_script and escoria.event_manager.is_channel_free("_front") \
or (
not escoria.event_manager.is_channel_free("_front") and \
not escoria.event_manager.get_running_event(
"_front"
).name == "load"
):
# If the room was loaded from change_scene and automatic transitions
# are not disabled, do the transition out now
@@ -159,10 +163,7 @@ func perform_script_events():
var ready_event_added: bool = false
# Run the ready event, if there is one.
if escoria.event_manager._running_event == null \
or (escoria.event_manager._running_event != null \
and escoria.event_manager._running_event.name != "load"):
ready_event_added = _run_script_event("ready")
ready_event_added = _run_script_event("ready")
if ready_event_added:
# Wait for ready event to be done

View File

@@ -1,9 +1,32 @@
# A tooltip displaying <verb> <item1> [<item2>]
tool
extends RichTextLabel
class_name ESCTooltip
# Maximum width of the label
const MAX_WIDTH = 200
# Minimum height of the label
const MIN_HEIGHT = 30
# Maximum height of the label
const MAX_HEIGHT = 500
# Height of one line in the label
const ONE_LINE_HEIGHT = 16
# Color of the label
export(Color) var color setget set_color
# Vector2 defining the offset from the cursor
export(Vector2) var offset_from_cursor = Vector2(10,0)
# Activates debug mode. If enabled, shows the label with a white background.
export(bool) var debug_mode = false setget set_debug_mode
# Infinitive verb
var current_action: String
@@ -19,32 +42,11 @@ var current_target2: String
# True if tooltip is waiting for a click on second target (use x with y)
var waiting_for_target2 = false
# Color of the label
export(Color) var color setget set_color
# Vector2 defining the offset from the cursor
export(Vector2) var offset_from_cursor = Vector2(10,0)
# Activates debug mode. If enabled, shows the label with a white background.
export(bool) var debug_mode = false setget set_debug_mode
# Node containing the debug white background
var debug_texturerect_node: TextureRect
# Maximum width of the label
const MAX_WIDTH = 200
# Minimum height of the label
const MIN_HEIGHT = 30
# Maximum height of the label
const MAX_HEIGHT = 500
# Height of one line in the label
const ONE_LINE_HEIGHT = 16
# Ready function
# Connect relevant functions
func _ready():
escoria.main.connect("room_ready", self, "_on_room_ready")
escoria.action_manager.connect("action_changed", self, "_on_action_selected")
@@ -84,12 +86,6 @@ func set_debug_mode(p_debug_mode: bool):
if debug_texturerect_node:
remove_child(debug_texturerect_node)
# Called when an action is selected in Escoria
func _on_action_selected() -> void:
current_action = escoria.action_manager.current_action
update_tooltip_text()
# Set the first target of the label.
#
@@ -122,22 +118,19 @@ func update_tooltip_text():
# Update the tooltip size according to the text.
func update_size():
## RECT_SIZE ##
if not get_tree():
# We're not in the tree anymore. Return
return
var rtl_width = rect_size.x
var rtl_height = rect_size.y
var content_height = get_content_height()
var nb_visible_characters = visible_characters
var nb_visible_lines = get_visible_line_count()
# printt("BEFORE", "text_height", content_height, "rtl_height", rect_size.y)
# if text is too long and is wrapped
# var nblines = float(get_content_height()) / float(ONE_LINE_HEIGHT)
var nblines = nb_visible_lines
if nblines >= 1:
yield(get_tree(), "idle_frame")
yield(get_tree(), "idle_frame")
var text_height = get_content_height()
if text_height > MAX_HEIGHT:
text_height = MAX_HEIGHT
@@ -151,7 +144,6 @@ func update_size():
rect_size.x += 1
parent_width = rect_size.x
rect_size.y = text_height
if rect_size.x >= MAX_WIDTH:
@@ -162,7 +154,6 @@ func update_size():
anchor_right = 0.0
anchor_bottom = 0.0
anchor_left = 0.0
# printt("AFTER", "text_height", get_content_height(), "rtl_height", rect_size.y)
# Calculate the offset of the label depending on its position.
@@ -235,3 +226,9 @@ func clear():
# Called when the room is loaded to setup the label.
func _on_room_ready():
escoria.main.current_scene.game.tooltip_node = self
# Called when an action is selected in Escoria
func _on_action_selected() -> void:
current_action = escoria.action_manager.current_action
update_tooltip_text()

View File

@@ -37,8 +37,9 @@ func get_re_group(re_match: RegExMatch, group: String) -> String:
# #### Parameters
#
# - value: The original value
# - type_hint: The type it should be
# **Returns** The typed value according to the type inference
func get_typed_value(value: String):
func get_typed_value(value: String, type_hint = []):
var regex_bool = RegEx.new()
regex_bool.compile("^true|false$")
var regex_float = RegEx.new()
@@ -52,7 +53,9 @@ func get_typed_value(value: String):
return int(value)
elif regex_bool.search(value.to_lower()):
return true if value.to_lower() == "true" else false
elif "," in value:
elif (typeof(type_hint) != TYPE_ARRAY and type_hint == TYPE_ARRAY) or \
(typeof(type_hint) == TYPE_ARRAY and TYPE_ARRAY in type_hint) \
and "," in value:
return value.split(",")
else:
return str(value)

View File

@@ -82,7 +82,6 @@ __meta__ = {
}
[node name="worker" parent="Hotspots" instance=ExtResource( 7 )]
pause_mode = 1
position = Vector2( 480, 430 )
esc_script = "res://game/rooms/room06/esc/worker.esc"
interaction_direction = 2

View File

@@ -27,3 +27,4 @@
:ready
queue_event worker moveworker

View File

@@ -0,0 +1,2 @@
:moveworker
walk worker worker_target

View File

@@ -1,4 +1,4 @@
[gd_scene load_steps=11 format=2]
[gd_scene load_steps=12 format=2]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc_terrain.gd" type="Script" id=1]
[ext_resource path="res://game/rooms/room14/background.tscn" type="PackedScene" id=2]
@@ -9,6 +9,7 @@
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc_item.gd" type="Script" id=7]
[ext_resource path="res://game/rooms/room14/r_door.tscn" type="PackedScene" id=8]
[ext_resource path="res://game/items/escitems/button.tscn" type="PackedScene" id=9]
[ext_resource path="res://game/characters/worker/worker.tscn" type="PackedScene" id=10]
[sub_resource type="NavigationPolygon" id=1]
vertices = PoolVector2Array( 1168.92, 640.557, 1182.53, 588.863, 1269.59, 622.872, 1275.03, 799.721, 864.626, 613.518, 1143.08, 613.35, -9.16094, 803.802, 386.666, 618.012, 129.634, 615.792, 84.5821, 654.06, -6.44019, 711.297, 3.15687, 646.051, 59.2201, 628.698 )
@@ -149,3 +150,12 @@ align = 1
__meta__ = {
"_edit_use_anchors_": false
}
[node name="worker" parent="." instance=ExtResource( 10 )]
position = Vector2( 204.268, 376.233 )
esc_script = "res://game/rooms/room14/esc/worker.esc"
[node name="worker_target" type="Position2D" parent="."]
position = Vector2( 970.51, 374.808 )
script = ExtResource( 5 )
global_id = "worker_target"

View File

@@ -390,6 +390,11 @@ _global_script_classes=[ {
"path": "res://addons/escoria-core/game/core-scripts/esc/commands/play_snd.gd"
}, {
"base": "ESCBaseCommand",
"class": "QueueEventCommand",
"language": "GDScript",
"path": "res://addons/escoria-core/game/core-scripts/esc/commands/queue_event.gd"
}, {
"base": "ESCBaseCommand",
"class": "QueueResourceCommand",
"language": "GDScript",
"path": "res://addons/escoria-core/game/core-scripts/esc/commands/queue_resource.gd"
@@ -611,6 +616,7 @@ _global_script_class_icons={
"InventoryAddCommand": "",
"InventoryRemoveCommand": "",
"PlaySndCommand": "",
"QueueEventCommand": "",
"QueueResourceCommand": "",
"RandGlobalCommand": "",
"RepeatCommand": "",