简单的 CSV→Resource 导入器(EditorScript/Plugin)
This commit is contained in:
parent
1690981c70
commit
85fadb21a1
70
addons/csv_resource_importer/CsvResourceImporter.cs
Normal file
70
addons/csv_resource_importer/CsvResourceImporter.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using Godot;
|
||||
|
||||
[Tool]
|
||||
public partial class CsvResourceImporter : EditorPlugin
|
||||
{
|
||||
private const string MenuName = "Import CSV Resources...";
|
||||
private EditorFileDialog _configDialog;
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
AddToolMenuItem(MenuName, Callable.From(OnImportMenu));
|
||||
|
||||
_configDialog = new EditorFileDialog
|
||||
{
|
||||
Access = EditorFileDialog.AccessEnum.Resources,
|
||||
FileMode = EditorFileDialog.FileModeEnum.OpenFile,
|
||||
Title = "Select CsvImportConfig"
|
||||
};
|
||||
_configDialog.AddFilter("*.tres,*.res ; Csv Import Config");
|
||||
_configDialog.FileSelected += OnConfigSelected;
|
||||
EditorInterface.Singleton.GetBaseControl().AddChild(_configDialog);
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
RemoveToolMenuItem(MenuName);
|
||||
if (_configDialog != null && IsInstanceValid(_configDialog))
|
||||
{
|
||||
_configDialog.FileSelected -= OnConfigSelected;
|
||||
_configDialog.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnImportMenu()
|
||||
{
|
||||
if (_configDialog == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_configDialog.PopupCentered(new Vector2I(900, 600));
|
||||
}
|
||||
|
||||
private void OnConfigSelected(string path)
|
||||
{
|
||||
var config = ResourceLoader.Load<CsvImportConfig>(path);
|
||||
if (config == null)
|
||||
{
|
||||
GD.PushError($"CSV importer: failed to load config: {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = CsvImporter.Import(config);
|
||||
if (result.ErrorCount > 0)
|
||||
{
|
||||
GD.PushWarning($"CSV importer: completed with {result.ErrorCount} errors, {result.CreatedCount} created, {result.SkippedCount} skipped.");
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
GD.PushWarning(error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.Print($"CSV importer: created {result.CreatedCount} resources, skipped {result.SkippedCount}.");
|
||||
}
|
||||
|
||||
var fileSystem = EditorInterface.Singleton.GetResourceFilesystem();
|
||||
fileSystem?.Scan();
|
||||
}
|
||||
}
|
||||
1
addons/csv_resource_importer/CsvResourceImporter.cs.uid
Normal file
1
addons/csv_resource_importer/CsvResourceImporter.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c60ywt2qwa2or
|
||||
6
addons/csv_resource_importer/plugin.cfg
Normal file
6
addons/csv_resource_importer/plugin.cfg
Normal file
@ -0,0 +1,6 @@
|
||||
[plugin]
|
||||
name="CSV Resource Importer"
|
||||
description="Import CSV data into Resource .tres files."
|
||||
author="Codex"
|
||||
version="1.0"
|
||||
script="CsvResourceImporter.cs"
|
||||
23
scripts/Data/Import/CsvImportConfig.cs
Normal file
23
scripts/Data/Import/CsvImportConfig.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Godot;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class CsvImportConfig : Resource
|
||||
{
|
||||
[Export(PropertyHint.File, "*.csv")]
|
||||
public string CsvPath { get; set; } = "";
|
||||
|
||||
[Export(PropertyHint.Dir)]
|
||||
public string OutputDir { get; set; } = "res://resources/data";
|
||||
|
||||
[Export]
|
||||
public Resource ResourceTemplate { get; set; }
|
||||
|
||||
[Export]
|
||||
public string IdColumn { get; set; } = "id";
|
||||
|
||||
[Export(PropertyHint.File, "*.tres,*.res")]
|
||||
public string IndexOutputPath { get; set; } = "";
|
||||
|
||||
[Export]
|
||||
public string ArrayDelimiter { get; set; } = ";";
|
||||
}
|
||||
1
scripts/Data/Import/CsvImportConfig.cs.uid
Normal file
1
scripts/Data/Import/CsvImportConfig.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cmrnp25roceut
|
||||
626
scripts/Data/Import/CsvImporter.cs
Normal file
626
scripts/Data/Import/CsvImporter.cs
Normal file
@ -0,0 +1,626 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
public class CsvImportResult
|
||||
{
|
||||
public int CreatedCount { get; set; }
|
||||
public int SkippedCount { get; set; }
|
||||
public int ErrorCount => Errors.Count;
|
||||
public List<string> Errors { get; } = new();
|
||||
|
||||
public void AddError(string message)
|
||||
{
|
||||
Errors.Add(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CsvImporter
|
||||
{
|
||||
private readonly struct CsvPropertyInfo
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly Variant.Type Type;
|
||||
public readonly PropertyHint Hint;
|
||||
public readonly string HintString;
|
||||
|
||||
public CsvPropertyInfo(string name, Variant.Type type, PropertyHint hint, string hintString)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
Hint = hint;
|
||||
HintString = hintString;
|
||||
}
|
||||
}
|
||||
|
||||
public static CsvImportResult Import(CsvImportConfig config)
|
||||
{
|
||||
var result = new CsvImportResult();
|
||||
if (config == null)
|
||||
{
|
||||
result.AddError("CSV importer: config is null.");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.CsvPath))
|
||||
{
|
||||
result.AddError("CSV importer: CsvPath is empty.");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (config.ResourceTemplate == null)
|
||||
{
|
||||
result.AddError("CSV importer: ResourceTemplate is not set.");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!config.CsvPath.StartsWith("res://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.AddError($"CSV importer: CsvPath must use res:// (got {config.CsvPath}).");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!FileAccess.FileExists(config.CsvPath))
|
||||
{
|
||||
result.AddError($"CSV importer: CsvPath not found: {config.CsvPath}");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.OutputDir))
|
||||
{
|
||||
result.AddError("CSV importer: OutputDir is empty.");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!config.OutputDir.StartsWith("res://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.AddError($"CSV importer: OutputDir must use res:// (got {config.OutputDir}).");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!EnsureOutputDir(config.OutputDir, result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var csvText = FileAccess.GetFileAsString(config.CsvPath);
|
||||
var rows = ParseCsv(csvText);
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
result.AddError("CSV importer: no rows found.");
|
||||
return result;
|
||||
}
|
||||
|
||||
var headers = rows[0];
|
||||
if (headers.Length == 0)
|
||||
{
|
||||
result.AddError("CSV importer: header row is empty.");
|
||||
return result;
|
||||
}
|
||||
|
||||
var headerMap = BuildHeaderMap(headers);
|
||||
if (headerMap.Count == 0)
|
||||
{
|
||||
result.AddError("CSV importer: header row has no usable columns.");
|
||||
return result;
|
||||
}
|
||||
|
||||
var idKey = Normalize(config.IdColumn);
|
||||
if (!headerMap.TryGetValue(idKey, out var idIndex))
|
||||
{
|
||||
result.AddError($"CSV importer: id column \"{config.IdColumn}\" not found.");
|
||||
return result;
|
||||
}
|
||||
|
||||
var indexIds = new Godot.Collections.Array<StringName>();
|
||||
var indexPaths = new Godot.Collections.Array<string>();
|
||||
var outputDir = config.OutputDir.TrimEnd('/');
|
||||
var template = config.ResourceTemplate;
|
||||
var propertyMap = BuildPropertyMap(template);
|
||||
|
||||
for (var rowIndex = 1; rowIndex < rows.Count; rowIndex++)
|
||||
{
|
||||
var row = rows[rowIndex];
|
||||
if (IsRowEmpty(row) || IsRowComment(row))
|
||||
{
|
||||
result.SkippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var idValue = GetCell(row, idIndex);
|
||||
if (string.IsNullOrWhiteSpace(idValue))
|
||||
{
|
||||
result.AddError($"CSV importer: row {rowIndex + 1} is missing id.");
|
||||
result.SkippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var resource = DuplicateTemplate(template, result);
|
||||
if (resource == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
for (var colIndex = 0; colIndex < headers.Length; colIndex++)
|
||||
{
|
||||
var header = headers[colIndex];
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cell = GetCell(row, colIndex);
|
||||
if (string.IsNullOrWhiteSpace(cell))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedHeader = Normalize(header);
|
||||
if (!propertyMap.TryGetValue(normalizedHeader, out var prop))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseCell(cell, prop, config.ArrayDelimiter, out var value))
|
||||
{
|
||||
result.AddError($"CSV importer: row {rowIndex + 1} column \"{header}\" has invalid value \"{cell}\".");
|
||||
continue;
|
||||
}
|
||||
|
||||
resource.Set(prop.Name, CreateVariant(value));
|
||||
}
|
||||
|
||||
var fileName = MakeSafeFileName(idValue);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
result.AddError($"CSV importer: row {rowIndex + 1} has invalid id \"{idValue}\".");
|
||||
result.SkippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputPath = $"{outputDir}/{fileName}.tres";
|
||||
var saveError = ResourceSaver.Save(resource, outputPath);
|
||||
if (saveError != Error.Ok)
|
||||
{
|
||||
result.AddError($"CSV importer: failed to save {outputPath} ({saveError}).");
|
||||
result.SkippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
indexIds.Add(new StringName(idValue));
|
||||
indexPaths.Add(outputPath);
|
||||
result.CreatedCount++;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.IndexOutputPath))
|
||||
{
|
||||
SaveIndex(config.IndexOutputPath, indexIds, indexPaths, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool EnsureOutputDir(string outputDir, CsvImportResult result)
|
||||
{
|
||||
var globalDir = ProjectSettings.GlobalizePath(outputDir);
|
||||
var dirError = DirAccess.MakeDirRecursiveAbsolute(globalDir);
|
||||
if (dirError != Error.Ok && dirError != Error.AlreadyExists)
|
||||
{
|
||||
result.AddError($"CSV importer: failed to create output dir {outputDir} ({dirError}).");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Resource DuplicateTemplate(Resource template, CsvImportResult result)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
result.AddError("CSV importer: ResourceTemplate is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var duplicated = template.Duplicate(true);
|
||||
if (duplicated is not Resource resource)
|
||||
{
|
||||
result.AddError("CSV importer: ResourceTemplate duplicate is not a Resource.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private static Variant CreateVariant(object value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
string s => Variant.CreateFrom(s),
|
||||
StringName s => Variant.CreateFrom(s),
|
||||
int i => Variant.CreateFrom(i),
|
||||
float f => Variant.CreateFrom(f),
|
||||
double d => Variant.CreateFrom(d),
|
||||
bool b => Variant.CreateFrom(b),
|
||||
Godot.Collections.Array a => Variant.CreateFrom(a),
|
||||
Godot.Collections.Dictionary d => Variant.CreateFrom(d),
|
||||
GodotObject o => Variant.CreateFrom(o),
|
||||
_ => Variant.CreateFrom(value.ToString())
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, CsvPropertyInfo> BuildPropertyMap(Resource resource)
|
||||
{
|
||||
var map = new Dictionary<string, CsvPropertyInfo>();
|
||||
var properties = resource.GetPropertyList();
|
||||
foreach (var entry in properties)
|
||||
{
|
||||
if (entry is not Godot.Collections.Dictionary dict)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var usage = (PropertyUsageFlags)(int)dict["usage"];
|
||||
if ((usage & PropertyUsageFlags.Storage) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = GetDictString(dict, "name");
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var type = (Variant.Type)(int)dict["type"];
|
||||
var hint = (PropertyHint)(int)dict["hint"];
|
||||
var hintString = GetDictString(dict, "hint_string");
|
||||
map[Normalize(name)] = new CsvPropertyInfo(name, type, hint, hintString);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> BuildHeaderMap(string[] headers)
|
||||
{
|
||||
var map = new Dictionary<string, int>();
|
||||
for (var i = 0; i < headers.Length; i++)
|
||||
{
|
||||
var normalized = Normalize(headers[i]);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!map.ContainsKey(normalized))
|
||||
{
|
||||
map[normalized] = i;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static string GetCell(string[] row, int index)
|
||||
{
|
||||
if (row == null || index < 0 || index >= row.Length)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return row[index].Trim();
|
||||
}
|
||||
|
||||
private static bool IsRowEmpty(string[] row)
|
||||
{
|
||||
if (row == null || row.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var cell in row)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(cell))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsRowComment(string[] row)
|
||||
{
|
||||
if (row == null || row.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var first = row[0].Trim();
|
||||
return first.StartsWith("#", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool TryParseCell(string raw, CsvPropertyInfo prop, string arrayDelimiter, out object value)
|
||||
{
|
||||
switch (prop.Type)
|
||||
{
|
||||
case Variant.Type.String:
|
||||
value = raw;
|
||||
return true;
|
||||
case Variant.Type.StringName:
|
||||
value = new StringName(raw);
|
||||
return true;
|
||||
case Variant.Type.Int:
|
||||
if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
{
|
||||
value = intValue;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case Variant.Type.Float:
|
||||
if (float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var floatValue))
|
||||
{
|
||||
value = floatValue;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case Variant.Type.Bool:
|
||||
if (TryParseBool(raw, out var boolValue))
|
||||
{
|
||||
value = boolValue;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case Variant.Type.Array:
|
||||
value = ParseArray(raw, prop, arrayDelimiter);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseBool(string raw, out bool value)
|
||||
{
|
||||
switch (raw.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "true":
|
||||
case "1":
|
||||
case "yes":
|
||||
case "y":
|
||||
value = true;
|
||||
return true;
|
||||
case "false":
|
||||
case "0":
|
||||
case "no":
|
||||
case "n":
|
||||
value = false;
|
||||
return true;
|
||||
default:
|
||||
value = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static object ParseArray(string raw, CsvPropertyInfo prop, string arrayDelimiter)
|
||||
{
|
||||
var items = raw.Split(arrayDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var elementType = GetArrayElementType(prop);
|
||||
switch (elementType)
|
||||
{
|
||||
case Variant.Type.StringName:
|
||||
var stringNameArray = new Godot.Collections.Array<StringName>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
stringNameArray.Add(new StringName(item));
|
||||
}
|
||||
return stringNameArray;
|
||||
case Variant.Type.Int:
|
||||
var intArray = new Godot.Collections.Array<int>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (int.TryParse(item, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
{
|
||||
intArray.Add(intValue);
|
||||
}
|
||||
}
|
||||
return intArray;
|
||||
case Variant.Type.Float:
|
||||
var floatArray = new Godot.Collections.Array<float>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (float.TryParse(item, NumberStyles.Float, CultureInfo.InvariantCulture, out var floatValue))
|
||||
{
|
||||
floatArray.Add(floatValue);
|
||||
}
|
||||
}
|
||||
return floatArray;
|
||||
default:
|
||||
var stringArray = new Godot.Collections.Array<string>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
stringArray.Add(item);
|
||||
}
|
||||
return stringArray;
|
||||
}
|
||||
}
|
||||
|
||||
private static Variant.Type GetArrayElementType(CsvPropertyInfo prop)
|
||||
{
|
||||
if (prop.Hint != PropertyHint.ArrayType || string.IsNullOrWhiteSpace(prop.HintString))
|
||||
{
|
||||
return Variant.Type.String;
|
||||
}
|
||||
|
||||
switch (prop.HintString.Trim())
|
||||
{
|
||||
case "StringName":
|
||||
return Variant.Type.StringName;
|
||||
case "String":
|
||||
return Variant.Type.String;
|
||||
case "int":
|
||||
case "Int":
|
||||
return Variant.Type.Int;
|
||||
case "float":
|
||||
case "Float":
|
||||
case "double":
|
||||
case "Double":
|
||||
return Variant.Type.Float;
|
||||
case "bool":
|
||||
case "Bool":
|
||||
return Variant.Type.Bool;
|
||||
default:
|
||||
return Variant.Type.String;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveIndex(string indexPath, Godot.Collections.Array<StringName> ids, Godot.Collections.Array<string> paths, CsvImportResult result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(indexPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!indexPath.StartsWith("res://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.AddError($"CSV importer: IndexOutputPath must use res:// (got {indexPath}).");
|
||||
return;
|
||||
}
|
||||
|
||||
var index = new CsvIndex
|
||||
{
|
||||
Ids = ids,
|
||||
Paths = paths
|
||||
};
|
||||
|
||||
var saveError = ResourceSaver.Save(index, indexPath);
|
||||
if (saveError != Error.Ok)
|
||||
{
|
||||
result.AddError($"CSV importer: failed to save index {indexPath} ({saveError}).");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string[]> ParseCsv(string text)
|
||||
{
|
||||
var rows = new List<string[]>();
|
||||
var row = new List<string>();
|
||||
var cell = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
|
||||
for (var i = 0; i < text.Length; i++)
|
||||
{
|
||||
var c = text[i];
|
||||
if (inQuotes)
|
||||
{
|
||||
if (c == '"')
|
||||
{
|
||||
if (i + 1 < text.Length && text[i + 1] == '"')
|
||||
{
|
||||
cell.Append('"');
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.Append(c);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
inQuotes = true;
|
||||
break;
|
||||
case ',':
|
||||
row.Add(cell.ToString());
|
||||
cell.Clear();
|
||||
break;
|
||||
case '\n':
|
||||
row.Add(cell.ToString());
|
||||
cell.Clear();
|
||||
rows.Add(row.ToArray());
|
||||
row.Clear();
|
||||
break;
|
||||
case '\r':
|
||||
break;
|
||||
default:
|
||||
cell.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.Length > 0 || row.Count > 0)
|
||||
{
|
||||
row.Add(cell.ToString());
|
||||
rows.Add(row.ToArray());
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static string Normalize(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim().TrimStart('\uFEFF');
|
||||
var sb = new StringBuilder(trimmed.Length);
|
||||
foreach (var c in trimmed)
|
||||
{
|
||||
if (c == '_' || c == '-' || c == ' ')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string MakeSafeFileName(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(value.Length);
|
||||
foreach (var c in value.Trim())
|
||||
{
|
||||
if (c == ' ' || c == '-' || c == '.')
|
||||
{
|
||||
sb.Append('_');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetDictString(Godot.Collections.Dictionary dict, string key)
|
||||
{
|
||||
if (!dict.ContainsKey(key))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return dict[key].AsString();
|
||||
}
|
||||
}
|
||||
1
scripts/Data/Import/CsvImporter.cs.uid
Normal file
1
scripts/Data/Import/CsvImporter.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dx0nkkhc357c3
|
||||
11
scripts/Data/Import/CsvIndex.cs
Normal file
11
scripts/Data/Import/CsvIndex.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Godot;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class CsvIndex : Resource
|
||||
{
|
||||
[Export]
|
||||
public Godot.Collections.Array<StringName> Ids { get; set; } = new();
|
||||
|
||||
[Export]
|
||||
public Godot.Collections.Array<string> Paths { get; set; } = new();
|
||||
}
|
||||
1
scripts/Data/Import/CsvIndex.cs.uid
Normal file
1
scripts/Data/Import/CsvIndex.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://hrwen2sp2api
|
||||
Loading…
Reference in New Issue
Block a user