626 lines
23 KiB
GDScript
626 lines
23 KiB
GDScript
@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
|