Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 4 additions & 26 deletions tools/check_orphan_assets.gd
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ extends EditorScript
## [code]load()[/code] calls are not recorded as dependencies. A path assembled
## at run time cannot be detected either way.

const Util = preload("./util.gd")

## Where to look for assets. Everything under it is checked.
const ROOT_FOLDER := "res://scenes/quests/story_quests/stella"

Expand All @@ -42,7 +44,7 @@ func _run() -> void:

var orphans: Dictionary[String, bool] = {}
var stale: Array[String] = []
for path: String in _all_files(ROOT_FOLDER):
for path: String in Util.all_files(ROOT_FOLDER, SKIP_FOLDERS):
if path.ends_with(IMPORT_SUFFIX):
# A .import without its source is a leftover, not an unused asset.
if not FileAccess.file_exists(path.trim_suffix(IMPORT_SUFFIX)):
Expand Down Expand Up @@ -91,7 +93,7 @@ func _collect_referenced() -> Dictionary[String, bool]:

var uid_or_path := RegEx.create_from_string("(?:uid|res)://[^\"')\\s]+")

for path: String in _all_files("res://"):
for path: String in Util.all_files("res://", SKIP_FOLDERS):
var extension := path.get_extension().to_lower()

if extension in RESOURCE_EXTENSIONS:
Expand All @@ -110,30 +112,6 @@ func _collect_referenced() -> Dictionary[String, bool]:
return referenced


## Returns every file under [param folder], recursively.
func _all_files(folder: String) -> PackedStringArray:
var files := PackedStringArray()
var pending := PackedStringArray([folder])

while not pending.is_empty():
var current := pending[-1]
pending.remove_at(pending.size() - 1)

if current in SKIP_FOLDERS:
continue

for directory: String in DirAccess.get_directories_at(current):
# Godot's own cache holds copies of everything and would count as
# references to assets that are otherwise unused.
if directory != ".godot":
pending.append(current.path_join(directory))

for file: String in DirAccess.get_files_at(current):
files.append(current.path_join(file))

return files


## Computes, for [param folder] and every folder recursively inside it, whether every
## asset file it (recursively) contains is in [param orphans], storing the result for
## each folder in [param fully_orphaned] as a set. A folder with no asset files anywhere
Expand Down
118 changes: 118 additions & 0 deletions tools/remove_csharp_related_files.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
@tool
extends EditorScript
## Removes the C# scripts, and the scenes and resources that use them
##
## Threadbare is written in GDScript only, so the C# variants that some addons
## ship are never loaded, and are unneeded weight. Run this in the editor after
## updating an addon, since the update brings them back. The report goes to the
## Output panel.
## [br][br]
## A scene or resource that uses a removed file is removed too, and so is anything
## that uses that scene, until nothing more is affected. Otherwise it would be
## left pointing at a file that is gone.

const Util = preload("./util.gd")

## Where to look for files. Everything under it is checked.
const ROOT_FOLDER := "res://"

const CSHARP_EXTENSION := "cs"

## Extensions of the files whose references to other files are followed.
const RESOURCE_EXTENSIONS: PackedStringArray = ["tscn", "tres"]


func _run() -> void:
var files := Util.all_files(ROOT_FOLDER)

var to_remove: Dictionary[String, bool] = {}
for path: String in files:
if path.get_extension() == CSHARP_EXTENSION:
to_remove[path] = true

var references: Dictionary[String, PackedStringArray] = {}
for path: String in files:
if path.get_extension() in RESOURCE_EXTENSIONS:
references[path] = _external_references(path)

# Each pass may mark for removal a scene that other scenes use, so this
# repeats until a pass adds nothing.
var grew := true
while grew:
grew = false
var gone := _identifiers_of(to_remove.keys())
for path: String in references:
if to_remove.has(path):
continue
for referenced: String in references[path]:
if gone.has(referenced):
to_remove[path] = true
grew = true
break

var removed: Array[String] = []
var paths: Array = to_remove.keys()
paths.sort()
for path: String in paths:
for target: String in [path, path + ".uid"]:
if not FileAccess.file_exists(target):
continue
var error := DirAccess.remove_absolute(ProjectSettings.globalize_path(target))
if error == OK:
removed.append(target)
else:
push_error("Could not remove %s: %s" % [target, error_string(error)])

var csharp := paths.filter(
func(path: String) -> bool: return path.get_extension() == CSHARP_EXTENSION
)
print(
(
"Removed %d C# script(s) and %d scene(s) or resource(s) using them, %d file(s) in total:"
% [csharp.size(), paths.size() - csharp.size(), removed.size()]
)
)
for path: String in removed:
print(" ", path)

if not removed.is_empty():
# The files changed behind the editor's back, so let it notice.
EditorInterface.get_resource_filesystem().scan()


## Returns the path and UID of every [code]ext_resource[/code] in [param path].
func _external_references(path: String) -> PackedStringArray:
var found_references := PackedStringArray()
var content := FileAccess.get_file_as_string(path)

var ext_resource := RegEx.create_from_string("(?m)^\\[ext_resource ([^\\]]*)\\]")
# The boundary keeps the id attribute from being read as part of uid.
var attribute := RegEx.create_from_string('\\b(path|uid)="([^"]+)"')

for line: RegExMatch in ext_resource.search_all(content):
for value: RegExMatch in attribute.search_all(line.get_string(1)):
found_references.append(value.get_string(2))

return found_references


## Returns the set of ways [param paths] can be referred to, which is their paths
## and their UIDs.
func _identifiers_of(paths: Array) -> Dictionary[String, bool]:
var identifiers: Dictionary[String, bool] = {}

for path: String in paths:
identifiers[path] = true

# A script keeps its UID next to it, in a .uid file.
var sidecar := path + ".uid"
if FileAccess.file_exists(sidecar):
identifiers[FileAccess.get_file_as_string(sidecar).strip_edges()] = true

var uid := ResourceUID.path_to_uid(path)
if uid != path:
identifiers[uid] = true

return identifiers
1 change: 1 addition & 0 deletions tools/remove_csharp_related_files.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bo5f7pq5bvc8p
28 changes: 26 additions & 2 deletions tools/util.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
@tool
extends Object

const DEBUG := true


## Return a list of all scenes in the project that match the filter.
## filter accepts one argument, the path to a scene, and returns a boolean,
Expand Down Expand Up @@ -49,3 +47,29 @@ static func find_scenes(
dir.list_dir_end()

return packed_scenes


## Return every file under [param folder], recursively. The folders in
## [param skip_folders], and everything under them, are left out. So is Godot's
## own .godot cache, which holds a copy of everything.
static func all_files(
folder: String, skip_folders: PackedStringArray = PackedStringArray()
) -> PackedStringArray:
var files := PackedStringArray()
var pending := PackedStringArray([folder])

while not pending.is_empty():
var current := pending[-1]
pending.remove_at(pending.size() - 1)

if current in skip_folders:
continue

for directory: String in DirAccess.get_directories_at(current):
if directory != ".godot":
pending.append(current.path_join(directory))

for file: String in DirAccess.get_files_at(current):
files.append(current.path_join(file))

return files
Loading