Skip to content

Instantly share code, notes, and snippets.

@disco0
Created August 28, 2022 20:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save disco0/f326abb3a0cc27dd388d4898ed117068 to your computer and use it in GitHub Desktop.
Save disco0/f326abb3a0cc27dd388d4898ed117068 to your computer and use it in GitHub Desktop.
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