Created
August 28, 2022 20:51
-
-
Save disco0/f326abb3a0cc27dd388d4898ed117068 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
tool | |
extends EditorScript | |
### | |
### Install (for Run usage) | |
### Copy to res://mod-scripts/RehydrateImports.gd | |
### | |
### Usage: | |
### var Rehydrate = load('res://mod-scripts/RehydrateImports.gd') | |
### | |
### # @param external: Use OutputDir as the target root directory instead of res:// | |
### # @param commit: Actually process/export resources. Will only list found items otherwise. | |
### Rehydrate.Run(external := false, commit := false) | |
### | |
const OK_PREV_COPY := 0 | |
const OutputDir = 'res://../rehydrate/' # Extension is expected format string value | |
# Defines one (or more) resolvable file extensions for pulling from import folder | |
class CachedResourceType: | |
var extensions: Array # String[] | |
var output_root: String = 'res://' | |
var dir: Directory = Directory.new() | |
func _init(_extensions, _output_root: String = 'res://'): | |
extensions = _extensions if typeof(_extensions) == TYPE_ARRAY else [ _extensions ] | |
output_root = _output_root | |
if not dir.dir_exists(output_root): | |
printerr('Configured output root is not an existing directory: %s' % [ output_root ]) | |
return | |
if extensions.empty(): | |
printerr('No file extensions passed to CachedResourceType.') | |
return | |
func is_target_resource_file(file_path: String) -> bool: | |
return extensions.find(file_path.get_extension()) > -1 | |
func is_target_resource_import_file(import_file_path: String) -> bool: | |
return ( | |
import_file_path.get_extension() == 'import' | |
and is_target_resource_file(import_file_path.get_basename()) | |
) | |
func is_target_resource_import_file_and_missing_orig(import_file_path: String) -> bool: | |
return ( | |
is_target_resource_import_file(import_file_path) | |
and not output_rooted_orig_exists(import_file_path) | |
) | |
func output_rooted_orig_exists(import_file_path: String) -> bool: | |
return dir.file_exists(to_output_rooted(import_file_path).get_basename()) | |
func to_output_rooted(res_path: String) -> String: | |
return res_path.replace('res://', output_root) | |
class ImportConfigFile extends ConfigFile: | |
func _init() -> void: | |
pass | |
var source_file: String setget , _get_source_file | |
func _get_source_file() -> String: | |
return get_value('deps', 'source_file', MISSING_VALUE) | |
var path: String setget , _get_path | |
func _get_path() -> String: | |
# @TODO: Configurable? | |
match _get_importer(): | |
'texture': | |
return get_value('remap', 'path.s3tc', | |
get_value('remap', 'path', MISSING_VALUE)) | |
_: | |
return get_value('remap', 'path', MISSING_VALUE) | |
var importer: String setget , _get_importer | |
func _get_importer() -> String: | |
return get_value('remap', 'importer', MISSING_VALUE) | |
const MISSING_VALUE := "\u0000" | |
class CachedResourceTypes: | |
var output_dir: String | |
var resolvers: Dictionary = { } | |
func _init(_output_dir: String = OutputDir) -> void: | |
output_dir = _output_dir | |
initialize_resolvers() | |
func initialize_resolvers() -> void: | |
resolvers = { | |
ogg = CachedResourceType.new('ogg', output_dir), | |
wav = CachedResourceType.new('wav', output_dir), | |
obj = CachedResourceType.new('obj', output_dir), | |
glb = CachedResourceType.new('glb', output_dir), | |
png = CachedResourceType.new('png', output_dir), | |
} | |
# Resolves to member name, or "" on failure. | |
func resolve_resource_type(import_file_path: String) -> String: | |
for member in resolvers.keys(): # [ 'ogg', 'wav', 'obj', 'glb', 'png' ]: | |
if resolvers.get(member).call('is_target_resource_import_file_and_missing_orig', | |
import_file_path): | |
return member | |
return "" | |
# <csquad-base>/project | |
# -> <csquad-base>/preload-convert/ogg | |
static func GetDefaultOutputRoot() -> String: | |
return ProjectSettings \ | |
.globalize_path('res://../') \ | |
.simplify_path() \ | |
.plus_file('rehydrate') | |
func _ready() -> void: | |
pass | |
#region Functions | |
var check_dir: Directory = Directory.new() | |
func check_output_dir_exists(output_path: String) -> bool: | |
var out_dir := output_path.get_base_dir() | |
if check_dir.dir_exists(out_dir): return true | |
if check_dir.file_exists(out_dir): | |
printerr('Output directory is an existing file path: <%s>' % [ out_dir ]) | |
return false | |
if OK != check_dir.make_dir_recursive(out_dir): | |
printerr('Error recursively creating output directory <%s>' % [ out_dir ]) | |
return false | |
return true | |
static func RerootResPath(path: String, new_root: String) -> String: | |
return path.replace('res://', new_root.trim_suffix('/') + '/') | |
# Current prop dict for processing: | |
# | |
# { | |
# importer: Import Type | |
# import_file: Resource's .import file | |
# output_import_file: `import_file` rerooted to output path | |
# imported: Resolved resource (created by Godot on import) | |
# path: Original source path | |
# output_path: `path` rerooted to output path | |
# } | |
# | |
# @TODO: Need to clean all this up after its working | |
var required_info_dict_keys := [ 'importer', 'import_file', 'output_import_file', 'imported', 'path', 'output_path' ] | |
const ERR_NOIMPORTER := '<UNKNOWN>' | |
func rehydrate_resource(info: Dictionary) -> bool: | |
if not(info.has_all(required_info_dict_keys)): | |
printerr('Info dict does not contain all required keys: %s' % [ PoolStringArray(required_info_dict_keys).join(', ') ]) | |
return false | |
var importer_type: String = info.get('importer', ERR_NOIMPORTER) | |
print('Processing import type: %s' % [ importer_type ]) | |
if importer_type == ERR_NOIMPORTER: | |
printerr('Pulled invalid imported key value after check.') | |
return false | |
# Prepare output dir | |
check_output_dir_exists(info.output_path) | |
var succ: bool = false | |
match importer_type: | |
'ogg': | |
succ = OK == rehydrate_ogg(info.imported, info.output_path) | |
'wav': | |
succ = OK == rehydrate_wav(info.imported, info.output_path) | |
'png': | |
succ = OK == rehydrate_png(info.path, info.output_path) | |
# Import types that don't have a reydration method, but still should have .import files copied | |
'glb', 'obj': | |
return OK == rehydrate_import_file_handler(info.import_file, info.output_import_file) | |
_: | |
push_warning('Unhandled import type: %s' % [ importer_type ]) | |
return false | |
if not succ: return succ | |
succ = OK == rehydrate_import_file_handler(info.import_file, info.output_import_file) | |
return succ | |
func rehydrate_import_file_handler(source_import_path, output_file_path) -> int: | |
if source_import_path == output_file_path: | |
#print('No copy necessary.') | |
return OK | |
if check_dir.file_exists(output_file_path): | |
return OK | |
var copy_err := check_dir.copy(source_import_path, output_file_path) | |
if copy_err != OK: | |
printerr('Error copying original .import file to rerooted output path: <%s>' % [ output_file_path ]) | |
return copy_err | |
# | |
# NOTE: Going to try loading png directly via resource loader, may need to add a fallback | |
# for stex | |
# | |
func rehydrate_png(source_path: String, output_path: String) -> int: | |
if check_dir.file_exists(output_path): | |
return OK_PREV_COPY | |
if not ResourceLoader.exists(source_path, 'StreamTexture'): | |
printerr('No valid streaming texture for source path: %s' % [ source_path ]) | |
var stex: StreamTexture = ResourceLoader.load(source_path, 'StreamTexture') | |
var save_err := stex.get_data().save_png(output_path) | |
if OK != save_err: | |
printerr(' [ERROR %d] Failed to write png data to output file: %s' % [ save_err, output_path ]) | |
return save_err | |
func rehydrate_ogg(oggstr_path: String, output_path: String) -> int: | |
if check_dir.file_exists(output_path): | |
return OK_PREV_COPY | |
var oggstr_file := File.new() | |
if OK != oggstr_file.open(oggstr_path, File.READ): | |
printerr('Failed to open %s' % [ oggstr_path ]) | |
return ERR_FILE_CANT_OPEN | |
var oggstr_len := oggstr_file.get_len() | |
# Skip header | |
oggstr_file.seek(279) | |
# Get everything after, minus the last 4 bytes? | |
# https://github.com/Bioruebe/godotdec/blob/master/godotdec/Program.cs#L83 | |
var ogg_buffer := oggstr_file.get_buffer(oggstr_len - oggstr_file.get_position()) | |
oggstr_file.close() | |
# Write to new file | |
var ogg_file := File.new() | |
if OK != ogg_file.open(output_path, File.WRITE): | |
printerr('Failed to open output file for write %s' % [ oggstr_path ]) | |
return ERR_FILE_CANT_OPEN | |
ogg_file.store_buffer(ogg_buffer) | |
ogg_file.close() | |
return OK | |
func rehydrate_wav(sample_path: String, output_path: String) -> int: | |
if check_dir.file_exists(output_path): | |
return OK_PREV_COPY | |
var loaded := ResourceLoader.load(sample_path) as AudioStreamSample | |
if not is_instance_valid(loaded): | |
push_warning('Failed to load AudioStreamSample resource from path: <%s>' % [ sample_path ]) | |
return ERR_INVALID_DATA | |
#print('Exporting %s' % [ output_path ]) | |
var save_err := loaded.save_to_wav(output_path) | |
if save_err != OK: | |
push_warning('[%04d] Error saving wav for %s' % [ save_err, output_path ]) | |
#error_count += 1 | |
# failed_sample_basenames.push_back(basename) | |
return save_err | |
func test(output_root: String = GetDefaultOutputRoot(), simulate := true) -> void: | |
var resolvers := CachedResourceTypes.new(output_root) | |
var resolved: String = "" | |
var import_config = ImportConfigFile.new() | |
var matches := [ ] | |
for path in get_flat_view_dict('res://'): | |
import_config.clear() | |
resolved = resolvers.resolve_resource_type(path) | |
if resolved.empty(): | |
#print(' X %s' % [ path ]) | |
continue | |
import_config.load(path) | |
matches.push_back({ | |
importer = resolved, | |
import_file = path, | |
output_import_file = RerootResPath(path, output_root), | |
imported = import_config.path, | |
path = import_config.source_file, | |
output_path = RerootResPath(import_config.source_file, output_root) | |
}) | |
#print(' -> %s' % [ path ]) | |
print('Would rehydate:') | |
for matched in matches: | |
print('%s\t=> %s' % [ matched.importer, matched.path ]) | |
print('\t %s' % [ matched.imported ]) | |
print('[%d total potential missing exports]' % [ matches.size() ]) | |
if(simulate): | |
print('[simulate parameter passed true, exiting before processing]') | |
return | |
var last_result: bool = false | |
var log_i := 0 | |
var total = matches.size() | |
for info in matches: | |
log_i += 1 | |
last_result = rehydrate_resource(info) | |
print('[%04d/%04d] %s %s' % [ | |
log_i, total, | |
'DONE ' if last_result else 'FAILED', | |
info.path | |
]) | |
static func Run(external := false, commit := false) -> void: | |
# 200iq | |
var gdscript := ResourceLoader.load("res://mod-scripts/RehydrateImports.gd", "GDScript", true) as GDScript | |
var instance := gdscript.new() as EditorScript | |
instance.test('res://' if external == false else GetDefaultOutputRoot(), not commit) | |
instance = null | |
gdscript = null | |
# https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e | |
static func get_flat_view_dict(p_dir = "res://", p_match = "", p_match_is_regex = false): | |
var regex = null | |
if p_match_is_regex: | |
regex = RegEx.new() | |
regex.compile(p_match) | |
if not regex.is_valid(): | |
print("regex failed to compile: ", p_match) | |
return [] | |
var dirs = [p_dir] | |
var first = true | |
var data = [] | |
while not dirs.empty(): | |
var dir = Directory.new() | |
var dir_name = dirs.back() | |
dirs.pop_back() | |
if dir.open(dir_name) == OK: | |
dir.list_dir_begin() | |
var file_name = dir.get_next() | |
while file_name != "": | |
if not dir_name == "res://": | |
first = false | |
# ignore hidden, temporary, or system content | |
if not file_name.begins_with(".") and not file_name.get_extension() in ["tmp"]: # , "import"]: | |
# If a directory, then add to list of directories to visit | |
if dir.current_is_dir(): | |
dirs.push_back(dir.get_current_dir() + "/" + file_name) | |
# If a file, check if we already have a record for the same name | |
else: | |
var path = dir.get_current_dir() + ("/" if not first else "") + file_name | |
# grab all | |
if not p_match: | |
data.append(path) | |
# grab matching strings | |
elif not p_match_is_regex and file_name.find(p_match, 0) != -1: | |
data.append(path) | |
# grab matching regex | |
else: | |
var regex_match = regex.search(path) | |
if regex_match != null: | |
data.append(path) | |
# Move on to the next file in this directory | |
file_name = dir.get_next() | |
# We've exhausted all files in this directory. Close the iterator. | |
dir.list_dir_end() | |
return data |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment