Skip to content

Commit

Permalink
feat: Introduce new scene cache management
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Adjust cache properties in configuration.tres and generate a cache map before export
  • Loading branch information
ThmsKnz authored and dploeger committed Nov 24, 2023
1 parent 11d1d5c commit 8db30fb
Show file tree
Hide file tree
Showing 12 changed files with 686 additions and 91 deletions.
264 changes: 264 additions & 0 deletions addons/egoventure/cache/cache_update_dialog.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
tool
class_name CacheUpdateDialog
extends WindowDialog


# List of scenes to be scanned in full run mode
var _scene_list: Array
# List of scenes to be scanned in delta run mode
var _scene_list_delta: Array

# Cache map which gets generated during scan
var _cache_map = CacheMap.new()

# Timestamp of last change date of cache_map.tres file
var _cache_map_time_modified: int


# Show the cache update dialog popup
func show_popup():
# Configure buttons
get_close_button().hide()
$VBox/HBoxContainer/RunDelta.set_disabled(false)
$VBox/HBoxContainer/RunFull.set_disabled(false)
$VBox/HBoxContainer/Cancel.set_disabled(false)
$VBox/HBoxContainer/RunDelta.call_deferred("grab_focus")

# Read scene directory path from configuration
var configuration = preload("res://configuration.tres")
var scene_dir = configuration.cache_scene_path

# Retrieve CacheMap and last modified timestamp
if ResourceLoader.exists("res://cache_map.tres"):
_cache_map = ResourceLoader.load("res://cache_map.tres")
var file = File.new()
_cache_map_time_modified = file.get_modified_time("res://cache_map.tres")
else:
_cache_map_time_modified = 0

# Retrieve list of scenes in scene directory
_scene_list.clear()
_scene_list_delta.clear()
_read_scene_list(scene_dir)

$VBox/SceneCount.text = "The project contains %s scenes. (%s of them were changed after last cache map update)" \
% [_scene_list.size(), _scene_list_delta.size()]
$VBox/ProgressBar.value = 0.0
self.popup_centered()
# Resize popup to ensure that it fits to rendered nodes
self.set_size($VBox.get_rect().size+Vector2(20,20))


# Start updating the cache map
#
# ** Parameters **
#
# - full_mode: indicates whether scan runs in full mode (=true)
# or delta mode (=false)
func _on_Run_pressed(full_mode: bool):
# Deactivate buttons
$VBox/HBoxContainer/RunDelta.set_disabled(true)
$VBox/HBoxContainer/RunFull.set_disabled(true)
$VBox/HBoxContainer/Cancel.set_disabled(true)
# "Verbose mode" button remains active and can be toggled

var scene_index = 0
var scene_list

if full_mode:
_cache_map.map.clear()
scene_list = _scene_list
else:
scene_list = _scene_list_delta

print("\nUpdate of Cache Map started")

yield(get_tree(),"idle_frame") # required for progress bar

# Iterate through all scenes
for scene_name in scene_list:
var scan_result = [0, [] ]
var linked_scenes = []

scene_index += 1
var scene = ResourceLoader.load(scene_name)
if scene.is_class("PackedScene"):
var scene_node = scene.instance()
if $VBox/HBoxContainer/Verbose.pressed:
print("Scan scene " + scene_name)
# Scan the scene to retrieve estimated size in kB and adjacent scenes
scan_result = _scan_scene(scene_node)
scene_node.free() # free up memory of instantiated scene
if $VBox/HBoxContainer/Verbose.pressed:
print("[size(kB), [scene list]] -> " + String(scan_result))
_cache_map.map[scene_name] = scan_result

$VBox/ProgressBar.set_value(float(scene_index) / scene_list.size() * 100)
yield(get_tree(),"idle_frame")

# Save result in fixed resource root directory
var err = ResourceSaver.save("res://cache_map.tres", _cache_map)
if err:
printerr("Saving res://cache_map.tres failed. Error Code %s" % err)
else:
print("Updated Cache Map successfully saved in res://cache_map.tres")

self.hide()
# Enable buttons
$VBox/HBoxContainer/RunDelta.set_disabled(false)
$VBox/HBoxContainer/RunFull.set_disabled(false)
$VBox/HBoxContainer/Cancel.set_disabled(false)


# Close popup when Cancel button is selected
func _on_Cancel_pressed():
self.hide()


# Recursively get all scene filenames from directory and subdirectories
#
# ** Parameters **
#
# - path: directory path
func _read_scene_list(path: String):
var dir = Directory.new()
var file = File.new()

if dir.open(path) == OK:
dir.list_dir_begin(true)
var filename = dir.get_next()
while filename != "":
if !dir.current_is_dir():
# Check whether file is a scene
if filename.match("*.tscn"):
# add to scene list
_scene_list.append(dir.get_current_dir() + "/" + filename)
# add to delta list if scene was modified after last cache map update
if file.get_modified_time(dir.get_current_dir() + "/" + filename) > _cache_map_time_modified:
_scene_list_delta.append(dir.get_current_dir() + "/" + filename)
# add to delta list if scene's gd script was modified after last cache map update
elif (
file.file_exists(dir.get_current_dir() + "/" + filename.get_basename() + ".gd") \
and file.get_modified_time(dir.get_current_dir() + "/" + filename.get_basename() + ".gd") \
> _cache_map_time_modified
):
_scene_list_delta.append(dir.get_current_dir() + "/" + filename)
else:
# Recursive call to process subdirectory
_read_scene_list(dir.get_current_dir() + "/" + filename)
filename = dir.get_next()
dir.list_dir_end()

else:
print("An error occurred when trying to open directory %s" % path)


# This will scan the scene and will return a size estimate and the adjacent scenes
#
# ** Parameters **
#
# - scene_node: root node of the scene
#
# **Returns** Array with 2 parameters:
# Array[0]: size estimate
# Array[1]: array of adjacent scenes
func _scan_scene(scene_node: Node) -> Array:
var size_estimate = 0
var linked_scenes: Array
var cache_include: String
var cache_exclude: String

# Regular expression to select all comments in script
# The capturing groups are used to ensure that '#' in quotations are not excluded
# Capturing group 1: all strings surrounded by double quotes
# Capturing group 3: all strings surrounded by single quotes
var regex_comment = RegEx.new()
regex_comment.compile("#.*|(\"(#.|[^\"])*\")|(\'(#.|[^\'])*\')")

# Regular expression for scene resources
var regex_scene = RegEx.new()
regex_scene.compile("res:\\/\\/[\\w\\/]*.tscn")

# get all sprites and target scenes listed in nodes
for node in _get_all_children(scene_node):

if node.get_class() == "Sprite" and node.texture != null:
if ResourceLoader.exists(node.texture.resource_path):
size_estimate += node.texture.get_data().get_data().size()

if ( node.get_class() == "TextureButton"
and "target_scene" in node # include Hotspot (and derived classes)
and not "loading_image" in node # but exclude MapHotspot
):
var scene_path = node.target_scene
if (
scene_path != ""
and ResourceLoader.exists(scene_path)
):
linked_scenes.append(scene_path)

# remove comments from source code
var scene_script = scene_node.get_script()
var source_code: String

if scene_script and scene_script.has_source_code():
source_code = scene_script.source_code
cache_include = ""
cache_exclude = ""

var regex_matches = regex_comment.search_all(source_code)
for i in range(regex_matches.size() - 1, -1, -1):
# replace regex match only if group 1 and 3 are empty
if regex_matches[i].strings[1] == "" and regex_matches[i].strings[3] == "":
source_code = source_code.substr(0, regex_matches[i].get_start()) + \
source_code.substr(regex_matches[i].get_end(), -1)
if regex_matches[i].strings[0].begins_with("#EVcache-include"):
# scene(s) listed in this comment will be included in cache
cache_include += regex_matches[i].strings[0]
elif regex_matches[i].strings[0].begins_with("#EVcache-exclude"):
# scene(s) listed in this comment will be excluded from cache
cache_exclude += regex_matches[i].strings[0]

# add scenes that need to be included to source code
source_code += cache_include

# scan remaining source code for scene names
var scene_matches = regex_scene.search_all(source_code)
for scene in scene_matches:
var scene_path = scene.get_string()
if ResourceLoader.exists(scene_path):
if not scene_path in linked_scenes:
linked_scenes.append(scene_path)
else:
print("Warning: %s: scene %s was not found" % [scene_script.resource_path, scene_path])

# process cache exclusion
var scene_exclude_matches = regex_scene.search_all(cache_exclude)
for scene in scene_exclude_matches:
var scene_path = scene.get_string()
if scene_path in linked_scenes:
linked_scenes.erase(scene_path)
else:
print("Warning: %s: scene %s that should be excluded from cache was not part of cache map" % [scene_script.resource_path, scene_path])

# convert size from Byte to kiloByte
size_estimate = size_estimate / 1024

return [size_estimate, linked_scenes]


# Recursive function to retrieve all child nodes
#
# ** Parameters **
#
# - node: starting node
#
# ** Returns ** list of all child nodes
func _get_all_children(node: Node)->Array:
var nodes: Array
for child in node.get_children():
nodes.append(child)
if child.get_child_count() > 0:
nodes.append_array(_get_all_children(child))
return nodes

92 changes: 92 additions & 0 deletions addons/egoventure/cache/cache_update_dialog.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
[gd_scene load_steps=2 format=2]

[ext_resource path="res://addons/egoventure/cache/cache_update_dialog.gd" type="Script" id=1]

[node name="CacheUpdateDialog" type="WindowDialog"]
margin_right = 791.0
margin_bottom = 186.0
size_flags_horizontal = 3
size_flags_vertical = 3
popup_exclusive = true
window_title = "EgoVenture - Update Cache Map"
script = ExtResource( 1 )

[node name="VBox" type="VBoxContainer" parent="."]
margin_left = 10.0
margin_top = 10.0
margin_right = 782.0
margin_bottom = 172.0
size_flags_horizontal = 3
size_flags_vertical = 3

[node name="Header" type="RichTextLabel" parent="VBox"]
margin_right = 772.0
margin_bottom = 15.0
text = "This will scan all scenes in configured scene folder looking for adjacent scenes that will be added to the cache map."
fit_content_height = true

[node name="SceneCount" type="RichTextLabel" parent="VBox"]
margin_top = 19.0
margin_right = 772.0
margin_bottom = 34.0
text = "This project contains x scenes."
fit_content_height = true

[node name="Note" type="RichTextLabel" parent="VBox"]
margin_top = 38.0
margin_right = 772.0
margin_bottom = 68.0
text = "Note: Depending on the size of the project the checks might run for several minutes. The result will be printed to Output / Console."
fit_content_height = true

[node name="Separator" type="HSeparator" parent="VBox"]
margin_top = 72.0
margin_right = 772.0
margin_bottom = 92.0
custom_constants/separation = 20

[node name="ProgressBar" type="ProgressBar" parent="VBox"]
margin_top = 96.0
margin_right = 772.0
margin_bottom = 110.0

[node name="Separator2" type="HSeparator" parent="VBox"]
margin_top = 114.0
margin_right = 772.0
margin_bottom = 134.0
custom_constants/separation = 20

[node name="HBoxContainer" type="HBoxContainer" parent="VBox"]
margin_top = 138.0
margin_right = 772.0
margin_bottom = 162.0
grow_horizontal = 0
custom_constants/separation = 20

[node name="RunDelta" type="Button" parent="VBox/HBoxContainer"]
margin_right = 205.0
margin_bottom = 24.0
text = "Update Cache Map (Delta Run)"

[node name="RunFull" type="Button" parent="VBox/HBoxContainer"]
margin_left = 225.0
margin_right = 419.0
margin_bottom = 24.0
text = "Update Cache Map (Full Run)"

[node name="Cancel" type="Button" parent="VBox/HBoxContainer"]
margin_left = 439.0
margin_right = 493.0
margin_bottom = 24.0
size_flags_horizontal = 2
text = "Cancel"

[node name="Verbose" type="CheckBox" parent="VBox/HBoxContainer"]
margin_left = 651.0
margin_right = 772.0
margin_bottom = 24.0
text = "Verbose mode"

[connection signal="pressed" from="VBox/HBoxContainer/RunDelta" to="." method="_on_Run_pressed" binds= [ false ]]
[connection signal="pressed" from="VBox/HBoxContainer/RunFull" to="." method="_on_Run_pressed" binds= [ true ]]
[connection signal="pressed" from="VBox/HBoxContainer/Cancel" to="." method="_on_Cancel_pressed"]
Loading

0 comments on commit 8db30fb

Please sign in to comment.