built more assets and started playing with foliage painting

This commit is contained in:
derek
2024-12-04 17:02:46 -06:00
parent dd960cc00e
commit 478e2822d2
359 changed files with 34172 additions and 178 deletions

View File

@@ -0,0 +1,625 @@
@tool
extends Node
#-------------------------------------------------------------------------------
# NOTE: automatic conversion from Godot 3.5 to Godot 4.0 will not be supported
# instead, open the original project in Godot 3.5, export transforms to JSON for each plant
# recreate plants in Godot 4.0 and import transforms one by one for each plant
#
# NOTE: most types that are represented as strings are kept in Godot 3.5 format
# this is deliberate, to preserve the state of converter as much as possible
#
# To use this converter:
# 1. Make sure the plugin is updated to the most recent version
# 2. Copy your scenes to addons/dreadpon.spatial_gardener/scene_converter/input_scenes folder.
# - Make sure they have a plain text scene file format (.tscn).
# - The scene converter automatically makes backups of your scenes. But you should make your own, in case anything goes wrong.
# 3. Editor might scream that there are resources missing. This is expected.
# - You might see a message that some plugin scripts are missing. Ignore, since some things *did* get removed in a plugin.
# - That's why you should *not* open these scenes for now.
# 4. Open the scene found at addons/dreadpon.spatial_gardener/scene_converter/scene_converter.tscn.
# 5. Launch it (F6 by default): it will start the conversion process.
# - The process takes about 1-10 minutes per scene, depending on it's size.
# 6. If any errors occured, you'll be notified in the console.
# - The editor will freeze for a while: the best way to keep track of your progress is by launching the editor from console
# - (or by running Godot_v***-stable_win64_console.cmd included in the official download).
# 7. If conversion was successful, grab your converted scenes from addons/dreadpon.spatial_gardener/scene_converter/output_scenes folder
# and move them to their intended places.
# 8. You should be able to launch your converted scenes now.
# - Optionally, you might have to relaunch the project and re-enable the plugin.
# - Make sure to move backups elsewhere before committing to source control.
#
# NOTE: your original scenes (in 'input_scenes' folder) should be intact
# but please keep a backup elsewhere just in case
#
# NOTE: to see the conversion status in real-time
# you'll need to launch editor with console, which you can then inspect
# this is done by launching Godot executable from native console/terminal
#-------------------------------------------------------------------------------
const Types = preload('converter_types.gd')
const Globals = preload("../utility/globals.gd")
const C_1_To_2 = preload('converters/c_1_to_2.gd')
const FunLib = preload("../utility/fun_lib.gd")
const Gardener = preload("../gardener/gardener.gd")
const Logger = preload('../utility/logger.gd')
const ConvertDialog_SCN = preload("convert_dialog.tscn")
const number_char_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-']
enum RunMode {RECREATE, DRY, CONVERT}
var logger = null
var conversion_map: Dictionary = {
1: {'target': 2, 'script': C_1_To_2.new()}
}
var run_mode = RunMode.CONVERT
var _base_control: Control = null
var _convert_dialog = null
var _result_dialog: AcceptDialog = null
#-------------------------------------------------------------------------------
# Lifecycle and events
#-------------------------------------------------------------------------------
func setup(__base_control: Control):
_base_control = __base_control
_scan_for_outdated_scenes()
func destroy():
if is_instance_valid(_convert_dialog):
_base_control.remove_child(_convert_dialog)
_convert_dialog.queue_free()
if is_instance_valid(_result_dialog):
_result_dialog.queue_free()
func _hide_dialog():
_convert_dialog.hide()
func _on_project_settings_changed():
_scan_for_outdated_scenes()
func _set_dont_scan_setting(val):
ProjectSettings.set("dreadpons_spatial_gardener/plugin/scan_for_outdated_scenes", !val)
func _ready():
if Engine.is_editor_hint(): return
_convert_from_input_dir()
#-------------------------------------------------------------------------------
# Conversion initiation
#-------------------------------------------------------------------------------
func _convert_from_input_dir():
var self_base_dir = get_script().resource_path.get_base_dir()
var in_path = self_base_dir + '/input_scenes'
var out_path = self_base_dir + '/output_scenes'
var canditate_scenes = _get_candidate_scenes(in_path, false)
if canditate_scenes.is_empty(): return
_run_conversion(canditate_scenes, true, out_path)
func _scan_for_outdated_scenes():
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/plugin/scan_for_outdated_scenes", true): return
var canditate_scenes = _get_candidate_scenes('res://')
if canditate_scenes.is_empty(): return
if !_convert_dialog:
_convert_dialog = ConvertDialog_SCN.instantiate()
_convert_dialog.confirm_pressed.connect(_convert_from_dialog)
_convert_dialog.confirm_pressed.connect(_hide_dialog)
_convert_dialog.cancel_pressed.connect(_hide_dialog)
_convert_dialog.dont_ask_again_toggled.connect(_set_dont_scan_setting)
if _convert_dialog.get_parent() != _base_control:
_base_control.add_child(_convert_dialog)
if !_result_dialog:
_result_dialog = AcceptDialog.new()
_result_dialog.title = 'Node3D Gardener conversion finished'
if _result_dialog.get_parent() != _base_control:
_base_control.add_child(_result_dialog)
_convert_dialog.add_scenes(canditate_scenes)
_convert_dialog.popup_centered()
func _convert_from_dialog():
var result = _run_conversion(_convert_dialog.get_selected_scenes(), _convert_dialog.should_mk_backups())
_result_dialog.dialog_text = (
"""Node3D Gardener conversion finished.
Please check the console/output for errors to see if conversion went successfully.
Don\'t forget to move the backups elsewhere before committing to version control.""")
_result_dialog.popup_centered()
#-------------------------------------------------------------------------------
# Scene candidate gathering
#-------------------------------------------------------------------------------
func _get_candidate_scenes(root_dir: String, check_gardeners: bool = true) -> Array:
var scene_file_paths = []
var gardener_file_paths = []
FunLib.iterate_files(root_dir, true, self, 'add_scene_file', scene_file_paths)
if !check_gardeners:
return scene_file_paths
var file = null
var text = ''
var gardener_regex = RegEx.new()
gardener_regex.compile('"class": "Gardener"')
var storage_regex = RegEx.new()
storage_regex.compile('storage_version = ([0-9])*?\n')
for scene_file in scene_file_paths:
file = FileAccess.open(scene_file, FileAccess.READ)
text = file.get_as_text()
file.close()
var results = gardener_regex.search_all(text)
if results.is_empty(): continue
results = storage_regex.search_all(text)
if results.is_empty():
gardener_file_paths.append(scene_file)
continue
for result in results:
if int(result.strings[1]) != Gardener.get_storage_ver() && conversion_map.has(result.strings[1]):
gardener_file_paths.append(scene_file)
continue
return gardener_file_paths
func add_scene_file(file_path: String, scenes: Array):
if file_path.get_extension() == 'tscn':
scenes.append(file_path)
#-------------------------------------------------------------------------------
# High-level conversion process
#-------------------------------------------------------------------------------
func _run_conversion(in_filepaths: Array, mk_backups: bool = true, out_base_dir: String = '') -> bool:
var timestamp = Time.get_datetime_string_from_system(false, true).replace(' ', '_').replace(':', '.')
logger = Logger.get_for(self, '', 'user://sg_tscn_conversion_%s.txt' % [timestamp])
logger.info('Found %d valid scenes for conversion' % [in_filepaths.size()])
for in_filepath in in_filepaths:
if mk_backups:
var num = 0
while FileAccess.file_exists('%s.backup_%d' % [in_filepath, num]):
num += 1
DirAccess.copy_absolute (in_filepath, '%s.backup_%d' % [in_filepath, num])
var out_filepath = in_filepath
if !out_base_dir.is_empty():
out_filepath = out_base_dir + '/' + in_filepath.get_file()
var start_time = Time.get_ticks_msec()
logger.info('Converting scene: "%s", to file: %s, backup: %s' % [in_filepath, out_filepath, mk_backups])
var in_size = 0
if run_mode == RunMode.CONVERT || run_mode == RunMode.RECREATE:
var file = FileAccess.open(in_filepath, FileAccess.READ)
in_size = file.get_length() * 0.000001
file.close()
var ext_res := {}
var sub_res := {}
logger.info('Parsing scene...')
var parsed_scene = parse_scene(in_filepath, ext_res, sub_res)
if run_mode == RunMode.CONVERT || run_mode == RunMode.DRY:
var storage_vers = get_vers(parsed_scene)
if storage_vers.size() < 1:
logger.warn('No Gardeners found in this scene')
continue
elif storage_vers.size() > 1:
logger.error('Gardeners in this scene have multiple mismatched storage versions. All Gardeners must be of the same version')
continue
var curr_ver = storage_vers[0]
while curr_ver != Gardener.get_storage_ver():
var conversion_data = conversion_map[curr_ver]
logger.info('Converting Gardener data from storage v.%s to v.%s...' % [curr_ver, conversion_data.target])
conversion_data.script.convert_gardener(parsed_scene, run_mode, ext_res, sub_res)
curr_ver = conversion_data.target
if run_mode == RunMode.CONVERT || run_mode == RunMode.RECREATE:
logger.info('Reconstructing scene...')
reconstruct_scene(parsed_scene, out_filepath)
var time_took = float(Time.get_ticks_msec() - start_time) / 1000
logger.info('Finished converting scene: "%s"' % [in_filepath])
logger.info('Took: %.2fs' % [ time_took])
if run_mode == RunMode.CONVERT || run_mode == RunMode.RECREATE:
var file = FileAccess.open(out_filepath, FileAccess.READ)
var out_size = file.get_length() * 0.000001
file.close()
logger.info('Size changed from %.2fMb to %.2fMb' % [in_size, out_size])
logger.info('Finished %d scene(s) conversions' % [in_filepaths.size()])
return true
func get_vers(parsed_scene):
var vers = []
for section in parsed_scene:
if section.props.get('__meta__', {}).get('class', '') == 'Gardener':
var ver = section.props.get('storage_version', 1)
if vers.has(ver): continue
vers.append(ver)
return vers
func reconstruct_scene(parsed_scene: Array, out_path: String):
var file = FileAccess.open(out_path, FileAccess.WRITE)
if !file:
logger.error('Unable to write to file "%s", with error: %s' % [out_path, Globals.get_err_message(FileAccess.get_open_error())])
var total_sections = float(parsed_scene.size())
var progress_milestone = 0
var last_type = ''
var section_num = 0
for section in parsed_scene:
if ['sub_resource', 'node'].has(last_type) || !last_type.is_empty() && last_type != section.type:
file.store_line('')
var line = '[' + section.type
for section_prop in section.header:
line += ' %s=%s' % [section_prop, Types.get_val_for_export(section.header[section_prop])]
line += ']'
file.store_line(line)
for prop in section.props:
line = '%s = %s' % [prop, Types.get_val_for_export(section.props[prop])]
file.store_line(line)
last_type = section.type
section_num += 1
var file_progress = floor(section_num / total_sections * 100)
if file_progress >= progress_milestone * 10:
logger.info('Reconstructed: %02d%%' % [progress_milestone * 10])
progress_milestone += 1
file.close()
#-------------------------------------------------------------------------------
# Low-level parsing
#-------------------------------------------------------------------------------
func parse_scene(filepath: String, ext_res: Dictionary = {}, sub_res: Dictionary = {}) -> Array:
var result := []
var file: FileAccess = FileAccess.open(filepath, FileAccess.READ)
if !file:
logger.error('Unable to open file "%s", with error: %s' % [filepath, Globals.get_err_message(FileAccess.get_open_error())])
var file_len = float(file.get_length())
var progress_milestone = 0
var section_string: PackedStringArray = PackedStringArray()
var section_active := false
var section = {}
var sections_parts = []
var open_square_brackets = 0
var header_start = 0
var header_active = false
var first_line = true
var line_byte_offset = 1
var line: String
while !file.eof_reached():
line = file.get_line()
var no_brackets = open_square_brackets == 0
var position = file.get_position()
var line_size = line.to_utf8_buffer().size() + line_byte_offset
# If first line size not equal to position - then we're dealing with CRLF
if first_line && position != line_size:
line_byte_offset = 2
line_size = line.to_utf8_buffer().size() + line_byte_offset
open_square_brackets += line.count('[')
open_square_brackets -= line.count(']')
if line.begins_with('['):
header_active = true
header_start = position - line_size
if header_active && open_square_brackets == 0:
open_square_brackets = 0
header_active = false
var header_end = position
file.seek(header_start)
var header_str = file.get_buffer(header_end - header_start).get_string_from_utf8().strip_edges()
file.seek(header_end)
section = {'type': '', 'header': {}, 'props': {}}
sections_parts = Array(header_str.trim_prefix('[').trim_suffix(']').split(' '))
section.type = sections_parts.pop_front()
section.header = parse_resource(" ".join(PackedStringArray(sections_parts)) + ' ', ' ')
result.append(section)
section_string = PackedStringArray()
if section.type == 'ext_resource':
ext_res[section.header.id] = section
elif section.type == 'sub_resource':
sub_res[section.header.id] = section
section_active = true
elif section_active && line.strip_escapes().is_empty() && !result.is_empty():
result[-1].props = parse_resource(''.join(section_string))
section_active = false
elif !line.strip_escapes().is_empty():
section_string.append(line + '\n')
var file_progress = floor(position / file_len * 100)
if file_progress >= progress_milestone * 10:
logger.info('Parsed: %02d%%' % [progress_milestone * 10])
progress_milestone += 1
if first_line:
first_line = false
return result
func parse_resource(res_string: String, separator: String = '\n') -> Dictionary:
if res_string.is_empty(): return {}
var result := {}
var tokens := tokenize_string(res_string, separator)
result = tokens_to_dict(tokens)
return result
func tokenize_string(string: String, separator: String = '\n') -> Array:
var tokens = Array()
var current_token = Types.Tokens.NONE
var character = ''
var status_bundle = {
'idx': 0,
'string': string,
'last_tokenized_idx': 0
}
for idx in string.length():
status_bundle.idx = idx
character = string[idx]
if current_token == Types.Tokens.NONE:
# All chars so far were numerical, and next one IS NOT
if ((string.length() <= idx + 1 || !number_char_list.has(string[idx + 1]))
&& str_has_only_numbers(str_last_inclusive(status_bundle))):
# Number string has a dot - is a float
if str_last_inclusive_stripped(status_bundle).find('.') >= 0:
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_REAL, float(str_last_inclusive_stripped(status_bundle))))
# Else - int
else:
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_INT,int(str_last_inclusive_stripped(status_bundle))))
status_bundle.last_tokenized_idx = idx + 1
if character == '=':
var prop_name = str_last_stripped(status_bundle)
while tokens.size() > 0:
var token_val = tokens[-1]
if token_val.type == Types.Tokens.STMT_SEPARATOR: break
tokens.pop_back()
prop_name = str(token_val.val) + prop_name
tokens.append(Types.TokenVal.new(Types.Tokens.PROP_NAME, prop_name))
tokens.append(Types.TokenVal.new(Types.Tokens.EQL_SIGN, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '"' && (idx == 0 || string[idx - 1] != '\\'):
current_token = Types.Tokens.DBL_QUOTE
status_bundle.last_tokenized_idx = idx + 1
elif character == ',':
tokens.append(Types.TokenVal.new(Types.Tokens.COMMA, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == ':':
tokens.append(Types.TokenVal.new(Types.Tokens.COLON, character))
status_bundle.last_tokenized_idx = idx + 1
# Parentheses not representing a "struct" are impossible
# So we don't parse them separately
elif character == '[':
tokens.append(Types.TokenVal.new(Types.Tokens.OPEN_SQR_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == ']':
tokens.append(Types.TokenVal.new(Types.Tokens.CLSD_SQR_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '{':
tokens.append(Types.TokenVal.new(Types.Tokens.OPEN_CLY_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '}':
tokens.append(Types.TokenVal.new(Types.Tokens.CLSD_CLY_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '(':
current_token = Types.Tokens.VAL_STRUCT
elif ['false', 'true'].has(str_last_inclusive_stripped(status_bundle).to_lower()):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_BOOL, Types.to_bool(str_last_inclusive_stripped(status_bundle))))
status_bundle.last_tokenized_idx = idx + 1
elif str_last_inclusive_stripped(status_bundle) == 'null':
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_NIL, null))
status_bundle.last_tokenized_idx = idx + 1
elif character == separator:
tokens.append(Types.TokenVal.new(Types.Tokens.STMT_SEPARATOR, ''))
elif current_token == Types.Tokens.DBL_QUOTE:
if character == '"' && (idx == 0 || string[idx - 1] != '\\'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_STRING, str_last(status_bundle)))
status_bundle.last_tokenized_idx = idx + 1
current_token = Types.Tokens.NONE
elif current_token == Types.Tokens.VAL_STRUCT && character == ')':
var str_struct = str_last_inclusive_stripped(status_bundle)
if str_struct.begins_with('SubResource'):
tokens.append(Types.TokenVal.new(Types.Tokens.SUB_RES, Types.SubResource.new(int(str_struct))))
elif str_struct.begins_with('ExtResource'):
tokens.append(Types.TokenVal.new(Types.Tokens.EXT_RES, Types.ExtResource.new(int(str_struct))))
elif str_struct.begins_with('Vector2'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR2, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Rect'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_RECT, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Vector3'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR3, Types.PS_Vector3.new(str_struct)))
elif str_struct.begins_with('Transform2D'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_TRANSFORM2D, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Plane'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_PLANE, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Quat'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_QUAT, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('AABB'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_AABB, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Basis'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_BASIS, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Transform'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_TRANSFORM, Types.PS_Transform.new(str_struct)))
elif str_struct.begins_with('Color'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_COLOR, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('NodePath'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_NODE_PATH, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolByteArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_RAW_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolIntArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_INT_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolRealArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_REAL_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolStringArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_STRING_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolVector2Array'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR2_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolVector3Array'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR3_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolColorArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_COLOR_ARRAY, Types.PropStruct.new(str_struct)))
else:
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_STRUCT, str_last_inclusive_stripped(status_bundle)))
status_bundle.last_tokenized_idx = idx + 1
current_token = Types.Tokens.NONE
return tokens
func str_last_stripped(status_bundle: Dictionary) -> String:
return str_last(status_bundle).strip_edges()
func str_last(status_bundle: Dictionary) -> String:
return status_bundle.string.substr(status_bundle.last_tokenized_idx, status_bundle.idx - status_bundle.last_tokenized_idx)
func str_last_inclusive_stripped(status_bundle: Dictionary) -> String:
return str_last_inclusive(status_bundle).strip_edges()
func str_last_inclusive(status_bundle: Dictionary) -> String:
return status_bundle.string.substr(status_bundle.last_tokenized_idx, status_bundle.idx - status_bundle.last_tokenized_idx + 1)
func str_has_only_numbers(string: String) -> bool:
string = string.strip_escapes().strip_edges()
if string.is_empty(): return false
for character in string:
if !number_char_list.has(character):
return false
return true
func tokens_to_dict(tokens: Array) -> Dictionary:
var result := {}
var keys := []
var nest_level := 1
var values := [result]
var dest_string = ''
var idx := 0
while idx < tokens.size():
var push_to_values := false
var token: Types.TokenVal = tokens[idx]
match token.type:
Types.Tokens.EQL_SIGN, Types.Tokens.COLON:
var key = values.pop_back()
keys.append(key)
Types.Tokens.CLSD_CLY_BRKT:
if values.size() > nest_level:
push_to_values = true
nest_level -= 1
Types.Tokens.CLSD_SQR_BRKT:
if values.size() > nest_level:
push_to_values = true
nest_level -= 1
Types.Tokens.COMMA:
push_to_values = true
Types.Tokens.PROP_NAME:
values.append(token.val)
Types.Tokens.OPEN_CLY_BRKT:
values.append({})
nest_level += 1
Types.Tokens.OPEN_SQR_BRKT:
values.append([])
nest_level += 1
Types.Tokens.STMT_SEPARATOR:
if tokens.size() <= idx + 1 || tokens[idx + 1].is_token(Types.Tokens.PROP_NAME):
push_to_values = true
_:
values.append(token.val)
if push_to_values:
var destination = values[-2]
var val = values.pop_back()
if destination is Array:
destination.append(val)
elif !keys.is_empty():
var key = keys.pop_back()
destination[key] = val
idx += 1
return result