└── addons └── csv-data-importer ├── LICENSE ├── array2d.gd ├── csv_data.gd ├── import_plugin.gd ├── plugin.cfg └── plugin.gd /addons/csv-data-importer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 citizenll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /addons/csv-data-importer/array2d.gd: -------------------------------------------------------------------------------- 1 | class_name Array2D 2 | # author: willnationsdev 3 | # description: A 2D Array class 4 | 5 | var data: Array = [] 6 | 7 | 8 | func _init(p_array: Array = [], p_deep_copy : bool = true): 9 | if p_deep_copy: 10 | for row in p_array: 11 | if row is Array: 12 | data.append(row.duplicate()) 13 | else: 14 | data = p_array 15 | 16 | 17 | func get_data() -> Array: 18 | return data 19 | 20 | 21 | func has_cell(p_row: int, p_col: int) -> bool: 22 | return len(data) > p_row and len(data[p_row]) > p_col 23 | 24 | 25 | func set_cell(p_row: int, p_col: int, p_value): 26 | assert(has_cell(p_row, p_col)) 27 | data[p_row][p_col] = p_value 28 | 29 | 30 | func get_cell(p_row: int, p_col: int): 31 | assert(has_cell(p_row, p_col)) 32 | return data[p_row][p_col] 33 | 34 | 35 | func set_cell_if_exists(p_row: int, p_col: int, p_value) -> bool: 36 | if has_cell(p_row, p_col): 37 | set_cell(p_row, p_col, p_value) 38 | return true 39 | return false 40 | 41 | 42 | func has_cellv(p_pos: Vector2) -> bool: 43 | return len(data) > p_pos.x and len(data[p_pos.x]) > p_pos.y 44 | 45 | 46 | func set_cellv(p_pos: Vector2, p_value): 47 | assert(has_cellv(p_pos)) 48 | data[p_pos.x][p_pos.y] = p_value 49 | 50 | 51 | func get_cellv(p_pos: Vector2): 52 | assert(has_cellv(p_pos)) 53 | return data[p_pos.x][p_pos.y] 54 | 55 | 56 | func set_cellv_if_exists(p_pos: Vector2, p_value) -> bool: 57 | if has_cellv(p_pos): 58 | set_cellv(p_pos, p_value) 59 | return true 60 | return false 61 | 62 | 63 | func get_row(p_idx: int): 64 | assert(len(data) > p_idx) 65 | assert(p_idx >= 0) 66 | return data[p_idx].duplicate() 67 | 68 | 69 | func get_col(p_idx: int): 70 | var result = [] 71 | for a_row in data: 72 | assert(len(a_row) > p_idx) 73 | assert(p_idx >= 0) 74 | result.push_back(a_row[p_idx]) 75 | return result 76 | 77 | 78 | func get_row_ref(p_idx: int): 79 | assert(len(data) > p_idx) 80 | assert(p_idx >= 0) 81 | return data[p_idx] 82 | 83 | 84 | func get_rows() -> Array: 85 | return data 86 | 87 | 88 | func set_row(p_idx: int, p_row): 89 | assert(len(data) > p_idx) 90 | assert(p_idx >= 0) 91 | assert(len(data) == len(p_row)) 92 | data[p_idx] = p_row 93 | 94 | 95 | func set_col(p_idx: int, p_col): 96 | assert(len(data) > 0 and len(data[0]) > 0) 97 | assert(len(data) == len(p_col)) 98 | var idx = 0 99 | for a_row in data: 100 | assert(len(a_row) > p_idx) 101 | assert(p_idx >= 0) 102 | a_row[p_idx] = p_col[idx] 103 | idx += 1 104 | 105 | 106 | func insert_row(p_idx: int, p_array: Array): 107 | if p_idx < 0: 108 | data.append(p_array) 109 | else: 110 | data.insert(p_idx, p_array) 111 | 112 | 113 | func insert_col(p_idx: int, p_array: Array): 114 | var idx = 0 115 | for a_row in data: 116 | if p_idx < 0: 117 | a_row.append(p_array[idx]) 118 | else: 119 | a_row.insert(p_idx, p_array[idx]) 120 | idx += 1 121 | 122 | 123 | func append_row(p_array: Array): 124 | insert_row(-1, p_array) 125 | 126 | 127 | func append_col(p_array: Array): 128 | insert_col(-1, p_array) 129 | 130 | 131 | func sort_row(p_idx: int): 132 | _sort_axis(p_idx, true) 133 | 134 | 135 | func sort_col(p_idx: int): 136 | _sort_axis(p_idx, false) 137 | 138 | 139 | func sort_row_custom(p_idx: int, p_func: Callable): 140 | _sort_axis_custom(p_idx, true, p_func) 141 | 142 | 143 | func sort_col_custom(p_idx: int, p_func: Callable): 144 | _sort_axis_custom(p_idx, false, p_func) 145 | 146 | 147 | func duplicate(): 148 | return load(get_script().resource_path).new(data) 149 | 150 | 151 | func hash() -> int: 152 | return hash(self) 153 | 154 | 155 | func shuffle(): 156 | for a_row in data: 157 | a_row.shuffle() 158 | 159 | 160 | func empty() -> bool: 161 | return len(data) == 0 162 | 163 | 164 | func size() -> int: 165 | if len(data) <= 0: 166 | return 0 167 | return len(data) * len(data[0]) 168 | 169 | 170 | func resize(p_height: int, p_width: int): 171 | data.resize(p_height) 172 | for i in range(len(data)): 173 | data[i] = [] 174 | data[i].resize(p_width) 175 | 176 | 177 | func resizev(p_dimensions: Vector2): 178 | resize(int(p_dimensions.x), int(p_dimensions.y)) 179 | 180 | 181 | func clear(): 182 | data = [] 183 | 184 | 185 | func fill(p_value): 186 | for a_row in range(data.size()): 187 | for a_col in range(data[a_row].size()): 188 | data[a_row][a_col] = p_value 189 | 190 | 191 | func fill_row(p_idx: int, p_value): 192 | assert(p_idx >= 0) 193 | assert(len(data) > p_idx) 194 | assert(len(data[0]) > 0) 195 | var arr = [] 196 | for i in len(data[0]): 197 | arr.push_back(p_value) 198 | data[p_idx] = arr 199 | 200 | 201 | func fill_col(p_idx: int, p_value): 202 | assert(p_idx >= 0) 203 | assert(len(data) > 0) 204 | assert(len(data[0]) > p_idx) 205 | var arr = [] 206 | for i in len(data): 207 | arr.push_back(p_value) 208 | set_col(p_idx, arr) 209 | 210 | 211 | func remove_row(p_idx: int): 212 | assert(p_idx >= 0) 213 | assert(len(data) > p_idx) 214 | data.remove_at(p_idx) 215 | 216 | 217 | func remove_col(p_idx: int): 218 | assert(len(data) > 0) 219 | assert(p_idx >= 0 and len(data[0]) > p_idx) 220 | for a_row in data: 221 | a_row.remove_at(p_idx) 222 | 223 | 224 | func count(p_value) -> int: 225 | var count = 0 226 | for a_row in data: 227 | for a_col in a_row: 228 | if p_value == data[a_row][a_col]: 229 | count += 1 230 | return count 231 | 232 | 233 | func has(p_value) -> bool: 234 | for a_row in data: 235 | for a_col in a_row: 236 | if p_value == data[a_row][a_col]: 237 | return true 238 | return false 239 | 240 | 241 | func invert(): 242 | data.reverse() 243 | return self 244 | 245 | 246 | func invert_row(p_idx: int): 247 | assert(p_idx >= 0 and len(data) > p_idx) 248 | data[p_idx].reverse() 249 | return self 250 | 251 | 252 | func invert_col(p_idx: int): 253 | assert(len(data) > 0) 254 | assert(p_idx >= 0 and len(data[0]) > p_idx) 255 | var col = get_col(p_idx) 256 | col.reverse() 257 | set_col(p_idx, col) 258 | return self 259 | 260 | 261 | func bsearch_row(p_idx: int, p_value, p_before: bool) -> int: 262 | assert(p_idx >= 0 and len(data) > p_idx) 263 | return data[p_idx].bsearch(p_value, p_before) 264 | 265 | 266 | func bsearch_col(p_idx: int, p_value, p_before: bool) -> int: 267 | assert(len(data) > 0) 268 | assert(p_idx >= 0 and len(data[0]) > p_idx) 269 | var col = get_col(p_idx) 270 | col.sort() 271 | return col[p_idx].bsearch(p_value, p_before) 272 | 273 | 274 | func find(p_value) -> Vector2: 275 | for a_row in data: 276 | for a_col in a_row: 277 | if p_value == data[a_row][a_col]: 278 | return Vector2(a_row, a_col) 279 | return Vector2(-1, -1) 280 | 281 | 282 | func rfind(p_value) -> Vector2: 283 | var i: int = len(data) - 1 284 | var j: int = len(data[0]) - 1 285 | while i: 286 | while j: 287 | if p_value == data[i][j]: 288 | return Vector2(i, j) 289 | j -= 1 290 | i -= 1 291 | return Vector2(-1, -1) 292 | 293 | 294 | func transpose(): 295 | var width : int = len(data) 296 | var height : int = len(data[0]) 297 | var transposed_matrix : Array 298 | for i in range(height): 299 | transposed_matrix.append([]) 300 | var h : int = 0 301 | while h < height: 302 | for w in range(width): 303 | transposed_matrix[h].append(data[w][h]) 304 | h += 1 305 | return load(get_script().resource_path).new(transposed_matrix, false) 306 | 307 | 308 | func _to_string() -> String: 309 | var ret: String 310 | var width: int = len(data) 311 | var height: int = len(data[0]) 312 | for h in range(height): 313 | for w in range(width): 314 | ret += "[" + str(data[w][h]) + "]" 315 | if w == width - 1 and h != height -1: 316 | ret += "\n" 317 | else: 318 | if w == width - 1: 319 | ret += "\n" 320 | else: 321 | ret += ", " 322 | return ret 323 | 324 | 325 | func _sort_axis(p_idx: int, p_is_row: bool): 326 | if p_is_row: 327 | data[p_idx].sort() 328 | return 329 | var col = get_col(p_idx) 330 | col.sort() 331 | set_col(p_idx, col) 332 | 333 | 334 | func _sort_axis_custom(p_idx: int, p_is_row: bool, p_func: Callable): 335 | if p_is_row: 336 | data[p_idx].sort_custom(p_func) 337 | return 338 | var col = get_col(p_idx) 339 | col.sort_custom(p_func) 340 | set_col(p_idx, col) 341 | -------------------------------------------------------------------------------- /addons/csv-data-importer/csv_data.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | 3 | ## column name 4 | @export var headers := [] 5 | ## origin data 6 | @export var records := [] : 7 | set(v): 8 | records = v 9 | if _auto_setup: 10 | setup() 11 | 12 | var _data:= {} #column name to index 13 | var _auto_setup = false 14 | var _initialed = false 15 | ## _data getter 16 | var data: 17 | get: 18 | return _data 19 | 20 | func _init(auto_setup = false): 21 | _auto_setup = auto_setup 22 | 23 | 24 | func setup(): 25 | if _initialed: 26 | push_warning(">_< csv file already setuped !") 27 | return self 28 | _initialed = true 29 | var field_indexs = {} 30 | for i in range(headers.size()): 31 | field_indexs[headers[i]] = i 32 | 33 | for i in range(headers.size()): 34 | for row in records: 35 | var primary_key = row[0] 36 | var row_data = {} 37 | for key in headers: 38 | var index = field_indexs[key] 39 | var value = row[index] 40 | row_data[key] = value 41 | _data[str(primary_key)]= row_data 42 | headers.clear() 43 | records.clear() 44 | 45 | return self 46 | 47 | 48 | func fetch(primary_key): 49 | return _data.get(str(primary_key)) 50 | 51 | 52 | func keys(): 53 | return _data.keys() 54 | -------------------------------------------------------------------------------- /addons/csv-data-importer/import_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorImportPlugin 3 | 4 | enum Presets { CSV } 5 | enum Delimiters { COMMA, TAB, SEMICOLON } 6 | 7 | const ALLOW_TYPES = ["str", "int", "float", "bool", "json"] 8 | 9 | func _get_importer_name(): 10 | return "citizenl.godot-csv-importer" 11 | 12 | 13 | func _get_visible_name(): 14 | return "CSV Data" 15 | 16 | 17 | func _get_priority(): 18 | # The built-in Translation importer needs a restart to switch to other importer 19 | return 2.0 20 | 21 | func _get_import_order(): 22 | return 0 23 | 24 | func _get_recognized_extensions(): 25 | return ["csv", "tsv"] 26 | 27 | 28 | func _get_save_extension(): 29 | return "tres" 30 | 31 | 32 | func _get_resource_type(): 33 | return "Resource" 34 | 35 | 36 | func _get_preset_count(): 37 | return Presets.size() 38 | 39 | 40 | func _get_preset_name(preset): 41 | match preset: 42 | Presets.CSV: 43 | return "CSV" 44 | _: 45 | return "Unknown" 46 | 47 | 48 | func _get_import_options(_path, preset): 49 | var delimiter = Delimiters.COMMA 50 | var headers = true 51 | return [ 52 | {name="delimiter", default_value=delimiter, property_hint=PROPERTY_HINT_ENUM, hint_string="Comma,Tab, Semicolon"}, 53 | {name="describe_headers", default_value=headers}, 54 | ] 55 | 56 | 57 | func _get_option_visibility(_path, option, options): 58 | return true # Godot does not update the visibility immediately 59 | if option == "force_float": 60 | return options.detect_numbers 61 | return true 62 | 63 | 64 | func _import(source_file, save_path, options, platform_variants, gen_files): 65 | var delim: String 66 | match options.delimiter: 67 | Delimiters.COMMA: 68 | delim = "," 69 | Delimiters.TAB: 70 | delim = "\t" 71 | Delimiters.SEMICOLON: 72 | delim = ";" 73 | 74 | var file = FileAccess.open(source_file, FileAccess.READ) 75 | 76 | if not file: 77 | printerr("Failed to open file: ", source_file) 78 | return FAILED 79 | 80 | var lines := Array2D.new() 81 | 82 | var meta = _parse_headers(file, options,delim) 83 | while not file.eof_reached(): 84 | var line = file.get_csv_line(delim) 85 | 86 | var row = _parse_typed(line, meta.headers, meta.field_types) 87 | if row==null or not row.size(): 88 | push_warning("[csv-importer]:csv row data null ",line) 89 | continue 90 | lines.append_row(row) 91 | 92 | file.close() 93 | 94 | # do not setup here 95 | var data = preload("csv_data.gd").new(false) 96 | var rows = lines.get_data() 97 | data.records = rows 98 | data.headers = meta.headers 99 | 100 | var filename = save_path + "." + _get_save_extension() 101 | var err = ResourceSaver.save(data, filename, ResourceSaver.FLAG_NONE) 102 | if err != OK: 103 | printerr("Failed to save resource: ", err) 104 | return err 105 | 106 | 107 | func _parse_headers(f: FileAccess, options,delim): 108 | var model_name = "" 109 | if options.describe_headers: 110 | var _desc = f.get_csv_line(delim) 111 | model_name= _desc[0] 112 | var headers = f.get_csv_line(delim) 113 | var types = f.get_csv_line(delim) 114 | # 115 | var field_indexs = {} 116 | var field_types = {} 117 | if headers[0] != "id": 118 | push_error("First column must be 'id'") 119 | return [] 120 | for i in range(headers.size()): 121 | field_indexs[headers[i]] = i 122 | 123 | for i in range(types.size()): 124 | field_types[headers[i]] = types[i] 125 | 126 | return {"model_name": model_name, "headers": headers,"types": types, "field_indexs": field_indexs, "field_types": field_types} 127 | 128 | 129 | func _parse_typed(csv_row: PackedStringArray, headers:PackedStringArray, types): 130 | var column = headers.size() 131 | if csv_row.size() != column: 132 | push_warning("[csv-importer]:csv row data not enough ",column," - > ",csv_row.size()," = ",csv_row) 133 | return [] 134 | var row = [] 135 | for i in range(headers.size()): 136 | var key = headers[i] 137 | var field_type = types[key] 138 | assert(field_type in ALLOW_TYPES) 139 | row.append(_parse_typed_value(csv_row[i], field_type)) 140 | return row 141 | 142 | 143 | func _parse_typed_value(p_value: String, p_type: String): 144 | match p_type: 145 | "str": 146 | return p_value 147 | "int": 148 | if p_value.is_empty(): 149 | p_value = "0" 150 | return int(p_value) 151 | "float": 152 | if p_value.is_empty(): 153 | p_value = "0" 154 | return float(p_value) 155 | "bool": 156 | if p_value.is_empty(): 157 | p_value = "false" #default is false 158 | return str_to_var(p_value) 159 | "json": 160 | if p_value.is_empty(): 161 | p_value = "[]" 162 | p_value = p_value.replace("`", "\"") 163 | return str_to_var(p_value) 164 | _: 165 | push_error("can not parse type ", p_type) 166 | return p_value 167 | -------------------------------------------------------------------------------- /addons/csv-data-importer/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="CSV Typed Importer" 4 | description="Import CSV files as native Array or Dictionary." 5 | author="citizenl" 6 | version="1.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/csv-data-importer/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | var import_plugin 6 | 7 | 8 | func _enter_tree(): 9 | import_plugin = preload("import_plugin.gd").new() 10 | add_import_plugin(import_plugin) 11 | 12 | 13 | func _exit_tree(): 14 | remove_import_plugin(import_plugin) 15 | import_plugin = null 16 | --------------------------------------------------------------------------------