fix(patata): added object-manager override. Solves #82

This commit is contained in:
2024-11-02 22:45:24 +01:00
parent d4f9cdb48f
commit bcf185231d
4 changed files with 616 additions and 9 deletions

View File

@@ -22,12 +22,12 @@ func configure() -> ESCCommandArgumentDescriptor:
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.has(arguments[0]):
escoria.logger.error(
self,
"Object %s not registered" % arguments[0]
)
return false
# if not escoria.object_manager.has(arguments[0]):
# escoria.logger.error(
# self,
# "Object %s not registered" % arguments[0]
# )
# return false
return .validate(arguments)

View File

@@ -0,0 +1,609 @@
# A manager for ESC objects
extends Resource
class_name ESCObjectManager
const CAMERA = "_camera"
const MUSIC = "_music"
const SOUND = "_sound"
const SPEECH = "_speech"
const RESERVED_OBJECTS = [
MUSIC,
SOUND,
SPEECH,
]
# The array of registered objects (organized by room, so each entry is a structure
# representing a room and its registered objects). This also includes one
# "room" for reserved objects; that is, we use one entry of the array to
# hold all reserved objects. This entry can be identified by the "is_reserved"
# property being set to true.
#
# "Reserved objects" are those which are named in the RESERVED_OBJECTS const
# array and include objects that are used internally by Escoria in every room,
# e.g. a music player, a sound player, a speech player, the main camera.
#
# In almost all cases, the reserved objects' entry doesn't need updating once
# created.
#
# Example structure:
#
# [
# {
# is_reserved: true, # Indicates this is the "reserved objects" entry
# room: "",
# room_instance_id: "",
# objects:
# {
# "_camera": camera
# },
# },
# {
# is_reserved: false, # Indicates this an entry for a room's objectss
# room_global_id: "<room_global_id>",
# room_instance_id: "<room_object_instance_id>",
# objects:
# {
# "obj1": val1,
# "obj2": val2
# }
# }
# ]
var room_objects: Array = []
# Array containing the encountered terrains so they can be properly saved in savegames
var room_terrains: Array = []
# We also store the current room's ids for retrieving the right objects.
var current_room_key: ESCRoomObjectsKey
# To avoid having to look this up all the time, we hold a reference.
var reserved_objects_container: ESCRoomObjects
func _init() -> void:
reserved_objects_container = ESCRoomObjects.new()
reserved_objects_container.is_reserved = true
reserved_objects_container.objects = {}
room_objects.push_back(reserved_objects_container)
current_room_key = ESCRoomObjectsKey.new()
# Updates which object manager room is to be treated as the currently active one.
#
# #### Parameters
#
# - room: Room to register the object with in the object manager
func set_current_room(room: ESCRoom) -> void:
if room == null:
escoria.logger.error(
self,
"Unable to set current room: No room was specified.\n" +
"Please pass in a valid ESCRoom as an argument to the method."
)
current_room_key.room_global_id = room.global_id
current_room_key.room_instance_id = room.get_instance_id()
# Register the object in the manager
#
# #### Parameters
#
# - object: Object to register
# - room: Room to register the object with in the object manager
# - force: Register the object, even if it has already been registered
# - auto_unregister: Automatically unregister object on tree_exited
func register_object(object: ESCObject, room: ESCRoom = null, force: bool = false, \
auto_unregister: bool = true) -> void:
if object.global_id.empty():
object.global_id = str(object.node.get_path()).split("/root/", false)[0]
object.node.global_id = object.global_id
escoria.logger.warn(
self,
"Registering ESCObject %s with empty global_id." % object.node.name +
"Using node's full path as global_id: %s"
% object.node.global_id
)
# If this is a reserved object, let's make sure it's in the right place.
# Note that we also don't allow it to auto unregister and, as such, we need
# to make sure we clean these up when the application exits.
if object.global_id in RESERVED_OBJECTS:
reserved_objects_container.objects[object.global_id] = object
return
var room_key: ESCRoomObjectsKey = ESCRoomObjectsKey.new()
# If a room was passed in, then we're going to register the object with it;
# otherwise, we register the object with the "current room".
if room == null or room.global_id.empty():
# We duplicate the key so as to not hold a reference when current_room_key
# changes.
if current_room_key.room_global_id.empty():
escoria.logger.error(
self,
"The current room has no Global ID.\n" +
"Please set the ESCRoom's Global ID property."
)
room_key.room_global_id = current_room_key.room_global_id
room_key.room_instance_id = current_room_key.room_instance_id
if not room_key.is_valid():
# This condition should very likely never happen.
escoria.logger.error(
self,
"No room was specified to register object with, and no current room is properly set.\n" +
"Please either pass in a valid ESCRoom to this method, or " + \
"call set_current_room() with a valid ESCRoom first."
)
else:
room_key.room_global_id = room.global_id
room_key.room_instance_id = room.get_instance_id()
if not force and _object_exists_in_room(object, room_key) \
and _object_state_in_room_is_default(object, room_key):
escoria.logger.warn(
self,
"Object with global id '%s' in room %s already registered from node path %s."
% [
object.global_id,
room_key.room_global_id,
get_object(object.global_id, room).node.get_path()
]
)
return
# Object exists in room, set it to is last state (if different from
# "default")
elif object.node is ESCItem and _object_exists_in_room(object, room_key):
# Object is already known, set its state to last known state
object.set_state(get_object(object.global_id).state)
# If the object is already connected, disconnect it for the case of
# forcing the registration, since we don't know if this object will be
# overwritten ("forced") in the future and, if it is, if it's set to
# auto-unregister or not. In most cases, objects are set to auto unregister.
if object.node.is_connected(
"tree_exited",
self,
"unregister_object"
):
object.node.disconnect(
"tree_exited",
self,
"unregister_object"
)
if force:
# If this ID already exists and we're about to overwrite it, do the
# safe thing and unregister the old object first
unregister_object_by_global_id(object.global_id, room_key)
if auto_unregister:
object.node.connect(
"tree_exited",
self,
"unregister_object",
[object, room_key]
)
if "is_interactive" in object.node and object.node.is_interactive:
object.interactive = true
if "esc_script" in object.node and not object.node.esc_script.empty():
var script = escoria.esc_compiler.load_esc_file(
object.node.esc_script
)
object.events = script.events
var objects: Dictionary = _get_room_objects_objects(room_key)
objects[object.global_id] = object
# If object state is not STATE_DEFAULT, save it in manager's object states
if object.state != ESCObject.STATE_DEFAULT:
if get_object(object.global_id) == null:
escoria.logger.error(
self,
"Object with global id %s in room (%s, %s) not found in Object Manager."
% [
object.global_id,
room_key.room_global_id,
room_key.room_instance_id
]
)
else:
get_object(object.global_id).state = object.state
# If this is the first object for the room, that means we have a brand new
# room and it needs to be setup and tracked.
if objects.size() == 1:
var room_container: ESCRoomObjects = ESCRoomObjects.new()
room_container.room_global_id = room_key.room_global_id
room_container.room_instance_id = room_key.room_instance_id
room_container.is_reserved = false
room_container.objects = objects
room_objects.push_back(room_container)
# Register the terrain in the manager
#
# #### Parameters
#
# - object: Object contianing the terrain to register
# - room: Room to register the object with in the object manager
func register_terrain(object: ESCObject, room: ESCRoom = null) -> void:
var room_key: ESCRoomObjectsKey = ESCRoomObjectsKey.new()
# If a room was passed in, then we're going to register the object with it;
# otherwise, we register the object with the "current room".
if not is_instance_valid(room) or room.global_id.empty():
# We duplicate the key so as to not hold a reference when current_room_key
# changes.
room_key.room_global_id = current_room_key.room_global_id
room_key.room_instance_id = current_room_key.room_instance_id
if not room_key.is_valid():
# This condition should very likely never happen.
escoria.logger.error(
self,
"No room was specified to register terrain with, and no current room is properly set.\n" +
"Please either pass in a valid ESCRoom to this method, or " + \
"call set_current_room() with a valid ESCRoom first."
)
else:
room_key.room_global_id = room.global_id
room_key.room_instance_id = room.get_instance_id()
var terrains: Dictionary = _get_room_terrain_navpolys(room_key)
if object.node is NavigationPolygonInstance:
terrains[object.global_id] = object
if terrains[object.global_id].node.enabled:
terrains[object.global_id].state = "enabled"
if terrains.size() == 1:
var room_container: ESCRoomTerrains = ESCRoomTerrains.new()
room_container.room_global_id = room_key.room_global_id
room_container.room_instance_id = room_key.room_instance_id
room_container.terrains = terrains
if room_terrains.has(room_container):
room_terrains.erase(room_container)
room_terrains.push_back(room_container)
# Check whether an object was registered
#
# #### Parameters
#
# - global_id: Global ID of object
# - room: ESCRoom instance the object is registered with.
# ***Returns*** Whether the object exists in the object registry
func has(global_id: String, room: ESCRoom = null) -> bool:
if global_id in RESERVED_OBJECTS:
if reserved_objects_container == null:
return false
return reserved_objects_container.objects.has(global_id)
var room_key: ESCRoomObjectsKey
if room == null:
room_key = current_room_key
else:
room_key = ESCRoomObjectsKey.new()
room_key.room_global_id = room.global_id
room_key.room_instance_id = room.get_instance_id()
if not _room_exists(room_key):
return false
var room_in_current_room = _object_exists_in_room(ESCObject.new(global_id, null), room_key)
if room_in_current_room:
return true
for room_container in room_objects:
room_key = ESCRoomObjectsKey.new()
room_key.room_global_id = room_container.room_global_id
room_key.room_instance_id = room_container.room_instance_id
if _object_exists_in_room(ESCObject.new(global_id, null), room_key):
return true
return false
# Get the object from the object registry
#
# #### Parameters
#
# - global_id: The global id of the object to retrieve
# - room: ESCRoom instance the object is registered with.
# ***Returns*** The retrieved object, or null if not found
func get_object(global_id: String, room: ESCRoom = null) -> ESCObject:
if global_id in RESERVED_OBJECTS:
if reserved_objects_container.objects.has(global_id):
return reserved_objects_container.objects[global_id]
else:
escoria.logger.warn(
self,
"Reserved object with global id %s not found in object manager!"
% global_id
)
return null
var room_key: ESCRoomObjectsKey
if room == null:
room_key = current_room_key
else:
room_key = ESCRoomObjectsKey.new()
room_key.room_global_id = room.global_id
room_key.room_instance_id = room.get_instance_id()
if not _room_exists(room_key):
escoria.logger.warn(
self,
"Specified room is empty/not found.\n" +
"Object with global id %s in room instance (%s, %s) not found."
% [global_id, room_key.room_global_id, room_key.room_instance_id]
)
return null
var objects: Dictionary = _get_room_objects_objects(room_key)
if objects.has(global_id):
return objects[global_id]
else:
escoria.logger.warn(
self,
"Object with global id %s in room instance (%s, %s) not found."
% [global_id, room_key.room_global_id, room_key.room_instance_id]
)
if escoria.inventory_manager.inventory_has(global_id):
# item is in the inventory and may be registered to a different room
for single_room in room_objects:
# these are arrays of the objects still registered for each room
if single_room.objects.has(global_id):
escoria.logger.info(
self,
"Object with global id %s found in room instance (%s, %s) through the inventory."
% [global_id, room_key.room_global_id, room_key.room_instance_id]
)
return single_room.objects[global_id]
return null
# Remove an object from the registry
#
# #### Parameters
#
# - object: The object to unregister
# - room_key: The room under which the object should be unregistered.
func unregister_object(object: ESCObject, room_key: ESCRoomObjectsKey) -> void:
if not _object_exists_in_room(object, room_key):
# Report this as a warning and not an error since this method may be
# called as part of an objectd's forced registration and the object not
# yet being managed.
escoria.logger.debug(
self,
"Unable to unregister object. " +
"Object with global ID %s room (%s, %s) not found. If this was "
% [
"?" if object == null else object.global_id,
room_key.room_global_id,
room_key.room_instance_id
] +
"part of a 'forced' registration, ignore this warning."
)
return
var room_objects = _get_room_objects_objects(room_key)
if not escoria.is_quitting and escoria.inventory_manager.inventory_has(object.global_id):
# Re-instance the node if it is an item present in inventory; that is,
# re-register it with the new current room.
if object.node != null:
object.node = object.node.duplicate()
register_object(object, null, true)
if object.state == ESCObject.STATE_DEFAULT:
room_objects.erase(object.global_id)
# If this room is truly empty, it's time to do away with it.
if room_objects.size() == 0:
_erase_room(room_key)
# Remove an object from the registry by global_id
#
# #### Parameters
#
# - global_id: The global_id of the object to unregister
# - room_key: The room under which the object should be unregistered.
func unregister_object_by_global_id(global_id: String, room_key: ESCRoomObjectsKey) -> void:
unregister_object(ESCObject.new(global_id, null), room_key)
# Insert data to save into savegame. For now, we only save the current room's
# objects.
#
# #### Parameters
#
# - p_savegame: The savegame resource
func save_game(p_savegame: ESCSaveGame) -> void:
p_savegame.objects = {}
for room_obj in room_objects:
if room_obj.room_global_id.empty():
continue
var room_objects_dict = {}
for obj_id in room_obj.objects:
var obj: ESCObject = room_obj.objects[obj_id]
var obj_json_to_save: Dictionary = obj.get_save_data()
if not obj_json_to_save.empty():
room_objects_dict[obj_id] = obj_json_to_save
p_savegame.objects[room_obj.room_global_id] = room_objects_dict
# Add in reserved objects (music, speech, sound), too.
var reserved_objects: Dictionary = reserved_objects_container.objects
for obj_global_id in reserved_objects:
if not reserved_objects[obj_global_id] is ESCObject:
continue
p_savegame.objects[obj_global_id] = reserved_objects[obj_global_id].get_save_data()
# Add ENABLED terrain navigationpolygons in, too.
p_savegame.terrain_navpolys = {}
for room_terrain_container in room_terrains:
if room_terrain_container.room_global_id == current_room_key.room_global_id:
for terrain_name in room_terrain_container.terrains:
var terrain_escobj = room_terrain_container.terrains[terrain_name]
if terrain_escobj.node.enabled:
p_savegame.terrain_navpolys[room_terrain_container.room_global_id] = {}
p_savegame.terrain_navpolys[room_terrain_container.room_global_id][terrain_name] = \
terrain_escobj.node.enabled
# Returns the current room's starting location. If more than one exists, the
# first one encountered is returned.
func get_start_location() -> ESCLocation:
if _room_exists(current_room_key):
for object in _get_room_objects_objects(current_room_key).values():
if is_instance_valid(object.node) \
and object.node is ESCLocation \
and object.node.is_start_location:
return object
escoria.logger.warn(
self,
"Room has no ESCLocation node with 'is_start_location' enabled. " +
"Player will be set at position (0,0)."
)
return null
# Determines whether 'container' represents the current room the player is in.
#
# #### Parameters
#
# - container: The entry in the object manager array being checked.
# **Returns** True iff container represents the the current room the player is in.
func _is_current_room(container: ESCRoomObjects) -> bool:
return _compare_container_to_key(container, current_room_key)
# Determines whether 'container' represents the room specified.
#
# #### Parameters
#
# - container: The entry in the object manager array being checked.
# - room_key: The key representing the desired room in the object manager array.
# **Returns** True iff container represents the object manager entry specified
# by room_key.
func _compare_container_to_key(container: ESCRoomContainer, room_key: ESCRoomObjectsKey) -> bool:
return container.room_global_id == room_key.room_global_id
# Checks whether an entry in the object manager array corresponds to the passed in
# room key.
#
# #### Parameters
#
# - room_key: The key representing the desired room in the object manager array.
# **Returns** True iff an entry in the object manager array corresponds to room_key.
func _room_exists(room_key: ESCRoomObjectsKey) -> bool:
for room_container in room_objects:
if _compare_container_to_key(room_container, room_key):
return true
return false
# Checks whether the specified object exists in the specified object manager entry.
#
# #### Parameters
#
# - object: The object to check for existence.
# - room_key: The key representing the desired room in the object manager array.
# **Returns** True iff object exists in the object manager entry specified by room_key.
func _object_exists_in_room(object: ESCObject, room_key: ESCRoomObjectsKey) -> bool:
if object == null:
escoria.logger.warn(
self,
"Cannot check room for \"null\" objects."
)
return false
for room_container in room_objects:
if _compare_container_to_key(room_container, room_key) \
and room_container.objects.has(object.global_id):
return true
return false
# Checks whether the specified object's state is "default" in the specified object manager entry.
#
# #### Parameters
#
# - object: The object to check for existence.
# - room_key: The key representing the desired room in the object manager array.
# **Returns** True if object's state is "default" in the object manager entry specified by room_key.
func _object_state_in_room_is_default(object: ESCObject, room_key: ESCRoomObjectsKey) -> bool:
if object == null:
escoria.logger.warn(
self,
"Cannot check room for \"null\" objects."
)
return false
for room_container in room_objects:
if _compare_container_to_key(room_container, room_key) \
and room_container.objects.has(object.global_id) \
and room_container.objects.get(object.global_id).state == ESCObject.STATE_DEFAULT:
return true
return false
# Returns the objects currently being managed in the object manager entry specified
# by the specified room key.
#
# #### Parameters
#
# - room_key: The key representing the desired room in the object manager array.
# **Returns** A reference to the dictionary of the entry's objects, or an empty
# dictionary otherwise.
func _get_room_objects_objects(room_key: ESCRoomObjectsKey) -> Dictionary:
for room_container in room_objects:
if _compare_container_to_key(room_container, room_key):
return room_container.objects
return {}
func _get_room_terrain_navpolys(room_key: ESCRoomObjectsKey) -> Dictionary:
for room_container in room_terrains:
if _compare_container_to_key(room_container, room_key):
return room_container.terrains
return {}
# Completely removes the entry in the object manager array specified by the room
# key.
#
# #### Parameters
#
# - room_key: The key representing the desired room in the object manager array.
func _erase_room(room_key: ESCRoomObjectsKey) -> void:
for room_container in room_objects:
if _compare_container_to_key(room_container, room_key):
room_objects.erase(room_container)
return

View File

@@ -1,7 +1,5 @@
:setup
# Uncomment to disable intro scene
#set_global cocina_delante_intro_played true
#set_global cocina_intro_played true
> [!cocina_delante_intro_played]
accept_input SKIP
stop_snd

View File

@@ -357,7 +357,7 @@ _global_script_classes=[ {
"base": "Resource",
"class": "ESCObjectManager",
"language": "GDScript",
"path": "res://addons/escoria-core/game/core-scripts/esc/esc_object_manager.gd"
"path": "res://addons/escoria-ui-return-monkey-island/overrides/esc_object_manager.gd"
}, {
"base": "ESCItem",
"class": "ESCPlayer",