Schema field editors (scalar, list, map, locale); entity editors show fields that other mods' schemas add to their kind
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ddb61d61e
commit
033192dcc9
16 files changed
+773
-2
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
tools/entity_editor/section_schema = Fields of other mods
|
||||||
|
tools/entity_editor/schema_hint = These fields are described in the schemas of the mods: {0}. The game reads them from the same JSON.
|
||||||
|
tools/schema/add_ref = + add…
|
||||||
|
tools/schema/list_add = + Add
|
||||||
|
tools/schema/list_remove = ✕
|
||||||
|
tools/schema/map_add = + Stat
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
tools/entity_editor/section_schema = Campos de otros mods
|
||||||
|
tools/entity_editor/schema_hint = Estos campos los describen los esquemas de los mods: {0}. El juego los lee del mismo JSON.
|
||||||
|
tools/schema/add_ref = + añadir…
|
||||||
|
tools/schema/list_add = + Añadir
|
||||||
|
tools/schema/list_remove = ✕
|
||||||
|
tools/schema/map_add = + Estadística
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
tools/entity_editor/section_schema = 他の MOD のフィールド
|
||||||
|
tools/entity_editor/schema_hint = これらのフィールドは次の MOD のスキーマが記述している:{0}。ゲームは同じ JSON から読む。
|
||||||
|
tools/schema/add_ref = + 追加…
|
||||||
|
tools/schema/list_add = + 追加
|
||||||
|
tools/schema/list_remove = ✕
|
||||||
|
tools/schema/map_add = + ステータス
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
tools/entity_editor/section_schema = 다른 모드의 필드
|
||||||
|
tools/entity_editor/schema_hint = 이 필드는 다음 모드들의 스키마가 설명합니다: {0}. 게임은 같은 JSON에서 읽습니다.
|
||||||
|
tools/schema/add_ref = + 추가…
|
||||||
|
tools/schema/list_add = + 추가
|
||||||
|
tools/schema/list_remove = ✕
|
||||||
|
tools/schema/map_add = + 능력치
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
tools/entity_editor/section_schema = Поля других модов
|
||||||
|
tools/entity_editor/schema_hint = Эти поля описали в своих схемах моды: {0}. Игра читает их из того же JSON.
|
||||||
|
tools/schema/add_ref = + добавить…
|
||||||
|
tools/schema/list_add = + Добавить
|
||||||
|
tools/schema/list_remove = ✕
|
||||||
|
tools/schema/map_add = + Стат
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
tools/entity_editor/section_schema = Trường của mod khác
|
||||||
|
tools/entity_editor/schema_hint = Các trường này do lược đồ của các mod mô tả: {0}. Game đọc chúng từ cùng tệp JSON.
|
||||||
|
tools/schema/add_ref = + thêm…
|
||||||
|
tools/schema/list_add = + Thêm
|
||||||
|
tools/schema/list_remove = ✕
|
||||||
|
tools/schema/map_add = + Chỉ số
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
tools/entity_editor/section_schema = 其他模组的字段
|
||||||
|
tools/entity_editor/schema_hint = 这些字段由以下模组的模式描述:{0}。游戏从同一个 JSON 读取它们。
|
||||||
|
tools/schema/add_ref = + 添加…
|
||||||
|
tools/schema/list_add = + 添加
|
||||||
|
tools/schema/list_remove = ✕
|
||||||
|
tools/schema/map_add = + 属性
|
||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"test_sources": [
|
"test_sources": [
|
||||||
"tests/*.cs"
|
"tests/*.cs"
|
||||||
],
|
],
|
||||||
"depends": ["dialog_base>=1", "interactive_base>=1", "time_base>=1", "map_base>=1", "ui_base>=1"],
|
"depends": ["dialog_base>=1", "interactive_base>=1", "time_base>=1", "map_base>=1", "ui_base>=1", "schema_base>=1"],
|
||||||
"test_suites": [
|
"test_suites": [
|
||||||
{"id": "windows", "activate": ["--itest=tools_windows"]},
|
{"id": "windows", "activate": ["--itest=tools_windows"]},
|
||||||
{"id": "ingame_play", "activate": ["--itest=ingame_play"]},
|
{"id": "ingame_play", "activate": ["--itest=ingame_play"]},
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ using Cthangover.Core.UI.Theming;
|
|||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Cthangover.Mods.Interface.Theming;
|
using Cthangover.Mods.Interface.Theming;
|
||||||
using Cthangover.Mods.Interface.Tool;
|
using Cthangover.Mods.Interface.Tool;
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
using Cthangover.Tools.Common;
|
using Cthangover.Tools.Common;
|
||||||
|
using Cthangover.Tools.Schemas;
|
||||||
using Cthangover.Tools.Services;
|
using Cthangover.Tools.Services;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
@@ -30,6 +32,12 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
/// editable, and every property the form does not know is shown as JSON under
|
/// editable, and every property the form does not know is shown as JSON under
|
||||||
/// "other fields", edited there and written back as it is.
|
/// "other fields", edited there and written back as it is.
|
||||||
/// <para/>
|
/// <para/>
|
||||||
|
/// Fields other mods describe for the kind in their schemas (<c>schemas/*.json</c>: the
|
||||||
|
/// equipment mod adds <c>Equip</c> and <c>Traits</c> to items, the stats add <c>Archetype</c>
|
||||||
|
/// and <c>Stats</c> to characters) get their own block, "fields of other mods", with the
|
||||||
|
/// schema editors, so a data editor grows with the mods loaded next to it without
|
||||||
|
/// knowing them.
|
||||||
|
/// <para/>
|
||||||
/// Every change is undoable (Ctrl+Z / Ctrl+Y): one undo step per editing
|
/// Every change is undoable (Ctrl+Z / Ctrl+Y): one undo step per editing
|
||||||
/// session of one entity, and one per add, duplicate or delete.
|
/// session of one entity, and one per add, duplicate or delete.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -44,6 +52,7 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
private readonly Dictionary<EntityField, TextureRect> previews = new();
|
private readonly Dictionary<EntityField, TextureRect> previews = new();
|
||||||
private readonly Dictionary<string, Texture2D> icons = new();
|
private readonly Dictionary<string, Texture2D> icons = new();
|
||||||
private readonly List<string> mods = new();
|
private readonly List<string> mods = new();
|
||||||
|
private readonly List<ISchemaFieldEditor> schemaEditors = new();
|
||||||
|
|
||||||
private OptionButton modPicker;
|
private OptionButton modPicker;
|
||||||
private FilteredList list;
|
private FilteredList list;
|
||||||
@@ -144,6 +153,7 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
idEdit.AddThemeFontOverride("font", ToolUi.Mono());
|
idEdit.AddThemeFontOverride("font", ToolUi.Mono());
|
||||||
foreach (var field in fields)
|
foreach (var field in fields)
|
||||||
BuildField(field);
|
BuildField(field);
|
||||||
|
BuildSchemaFields();
|
||||||
|
|
||||||
form.AddChild(ToolUi.Section("tools/entity_editor/section_other"));
|
form.AddChild(ToolUi.Section("tools/entity_editor/section_other"));
|
||||||
form.AddChild(ToolUi.Hint(ToolUi.Tr("tools/entity_editor/other_hint")));
|
form.AddChild(ToolUi.Hint(ToolUi.Tr("tools/entity_editor/other_hint")));
|
||||||
@@ -188,6 +198,20 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildSchemaFields()
|
||||||
|
{
|
||||||
|
var described = SchemaCatalog.Current.FieldsOf(kind)
|
||||||
|
.Where(f => !string.Equals(f.Key, "Id", StringComparison.OrdinalIgnoreCase) && !fields.Any(e => string.Equals(e.Key, f.Key, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
if (described.Count == 0)
|
||||||
|
return;
|
||||||
|
form.AddChild(ToolUi.Section("tools/entity_editor/section_schema"));
|
||||||
|
var sources = string.Join(", ", described.Select(f => f.SourceMod).Where(m => m != null).Distinct());
|
||||||
|
form.AddChild(ToolUi.Hint(string.Format(ToolUi.Tr("tools/entity_editor/schema_hint"), sources)));
|
||||||
|
foreach (var field in described)
|
||||||
|
schemaEditors.Add(SchemaFieldEditors.Create(form, field, WriteForm));
|
||||||
|
}
|
||||||
|
|
||||||
private void BuildPicture(EntityField field)
|
private void BuildPicture(EntityField field)
|
||||||
{
|
{
|
||||||
var row = new HBoxContainer();
|
var row = new HBoxContainer();
|
||||||
@@ -257,6 +281,8 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
languages = LocaleFileService.Languages(mod);
|
languages = LocaleFileService.Languages(mod);
|
||||||
foreach (var field in fields.Where(f => f.Kind == EntityFieldKind.LocaleText))
|
foreach (var field in fields.Where(f => f.Kind == EntityFieldKind.LocaleText))
|
||||||
BuildTextRows(field);
|
BuildTextRows(field);
|
||||||
|
foreach (var locale in schemaEditors.OfType<LocaleFieldEditor>())
|
||||||
|
locale.Open(document, mod, languages);
|
||||||
MarkClean();
|
MarkClean();
|
||||||
selected = null;
|
selected = null;
|
||||||
RefreshList();
|
RefreshList();
|
||||||
@@ -337,6 +363,8 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
idEdit.Text = record.Id;
|
idEdit.Text = record.Id;
|
||||||
foreach (var field in fields)
|
foreach (var field in fields)
|
||||||
ShowField(field, record.Data);
|
ShowField(field, record.Data);
|
||||||
|
foreach (var editor in schemaEditors)
|
||||||
|
editor.Load(record.Data.FirstOrDefault(p => string.Equals(p.Key, editor.Field.Key, StringComparison.OrdinalIgnoreCase)).Value);
|
||||||
otherEdit.Text = OtherJson(record.Data);
|
otherEdit.Text = OtherJson(record.Data);
|
||||||
suppress = false;
|
suppress = false;
|
||||||
}
|
}
|
||||||
@@ -451,6 +479,15 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
data["Id"] = idEdit.Text.Trim();
|
data["Id"] = idEdit.Text.Trim();
|
||||||
foreach (var field in fields)
|
foreach (var field in fields)
|
||||||
WriteField(field, data);
|
WriteField(field, data);
|
||||||
|
foreach (var editor in schemaEditors)
|
||||||
|
{
|
||||||
|
var key = data.FirstOrDefault(p => string.Equals(p.Key, editor.Field.Key, StringComparison.OrdinalIgnoreCase)).Key ?? editor.Field.Key;
|
||||||
|
var value = editor.Save();
|
||||||
|
if (value == null)
|
||||||
|
data.Remove(key);
|
||||||
|
else
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
SetDirty();
|
SetDirty();
|
||||||
RefreshList();
|
RefreshList();
|
||||||
}
|
}
|
||||||
@@ -524,7 +561,8 @@ namespace Cthangover.Tools.EntityEditor
|
|||||||
return EntityDocument.Serialize(other).TrimEnd();
|
return EntityDocument.Serialize(other).TrimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsFormKey(string key) => key == "Id" || fields.Any(f => f.Key == key);
|
private bool IsFormKey(string key)
|
||||||
|
=> key == "Id" || fields.Any(f => f.Key == key) || schemaEditors.Any(e => string.Equals(e.Field.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
private void OnOtherChanged()
|
private void OnOtherChanged()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Cthangover.Tools.Schemas
|
||||||
|
{
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
/// <summary>One field's editor in the data editor form: shows the field's JSON and gives it back edited.</summary>
|
||||||
|
public interface ISchemaFieldEditor
|
||||||
|
{
|
||||||
|
/// <summary>The described field.</summary>
|
||||||
|
FieldSchema Field { get; }
|
||||||
|
|
||||||
|
/// <summary>Shows <paramref name="node"/> (null: the field is absent).</summary>
|
||||||
|
void Load(JsonNode node);
|
||||||
|
|
||||||
|
/// <summary>The edited value, or null to remove the key.</summary>
|
||||||
|
JsonNode Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
namespace Cthangover.Tools.Schemas
|
||||||
|
{
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Cthangover.Mods.Interface.Theming;
|
||||||
|
using Cthangover.Tools.Common;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Edits a <c>list</c> field — an array of objects described by <see cref="FieldSchema.Of"/>
|
||||||
|
/// (an item's traits, an archetype's learnings, a shop's goods) — as a stack of small
|
||||||
|
/// cards with an add and a remove button, each card a form of the sub-fields.
|
||||||
|
/// <para/>
|
||||||
|
/// The <c>values</c> type is the same editor over the engine's older list of
|
||||||
|
/// <c>"Key=Value"</c> strings (a battle action's <c>Values</c>): known keys become fields,
|
||||||
|
/// entries the schema does not know are kept as they were.
|
||||||
|
/// </summary>
|
||||||
|
public class ListFieldEditor : ISchemaFieldEditor
|
||||||
|
{
|
||||||
|
private readonly FieldSchema field;
|
||||||
|
private readonly Action changed;
|
||||||
|
private readonly VBoxContainer cards;
|
||||||
|
private readonly List<List<SchemaFieldControl>> rows = new();
|
||||||
|
private readonly List<string> unknownValues = new();
|
||||||
|
private bool suppress;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public FieldSchema Field => field;
|
||||||
|
|
||||||
|
/// <summary>Adds the editor for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||||
|
public ListFieldEditor(Container parent, FieldSchema field, Action changed)
|
||||||
|
{
|
||||||
|
this.field = field;
|
||||||
|
this.changed = changed;
|
||||||
|
var box = new VBoxContainer { Name = "List_" + field.Key };
|
||||||
|
box.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||||
|
cards = new VBoxContainer();
|
||||||
|
cards.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||||
|
box.AddChild(cards);
|
||||||
|
var add = ToolUi.Button("tools/schema/list_add", AddEmpty, ButtonKind.Secondary);
|
||||||
|
add.Name = "Add";
|
||||||
|
add.SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin;
|
||||||
|
box.AddChild(add);
|
||||||
|
ToolUi.Field(parent, string.IsNullOrEmpty(field.Name) ? field.Key : field.Name, box, field.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsValues => string.Equals(field.Type, "values", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>Shows the array <paramref name="node"/> (null or not an array: empty).</summary>
|
||||||
|
public void Load(JsonNode node)
|
||||||
|
{
|
||||||
|
suppress = true;
|
||||||
|
Clear();
|
||||||
|
unknownValues.Clear();
|
||||||
|
if (node is JsonArray array)
|
||||||
|
{
|
||||||
|
if (IsValues)
|
||||||
|
LoadValues(array);
|
||||||
|
else
|
||||||
|
foreach (var item in array.OfType<JsonObject>())
|
||||||
|
AddCard(item);
|
||||||
|
}
|
||||||
|
suppress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The edited array, or null when it has no entries (the key is then removed).</summary>
|
||||||
|
public JsonNode Save()
|
||||||
|
{
|
||||||
|
var array = new JsonArray();
|
||||||
|
if (IsValues)
|
||||||
|
{
|
||||||
|
foreach (var card in rows)
|
||||||
|
{
|
||||||
|
foreach (var control in card.Where(c => c.Value.Length > 0))
|
||||||
|
array.Add(control.Field.Key + "=" + control.Value);
|
||||||
|
}
|
||||||
|
foreach (var raw in unknownValues)
|
||||||
|
array.Add(raw);
|
||||||
|
return array.Count == 0 ? null : array;
|
||||||
|
}
|
||||||
|
foreach (var card in rows)
|
||||||
|
{
|
||||||
|
var item = new JsonObject();
|
||||||
|
foreach (var control in card.Where(c => c.Value.Length > 0))
|
||||||
|
item[control.Field.Key] = SchemaJson.ToNode(control.Field, control.Value);
|
||||||
|
if (item.Count > 0)
|
||||||
|
array.Add(item);
|
||||||
|
}
|
||||||
|
return array.Count == 0 ? null : array;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadValues(JsonArray array)
|
||||||
|
{
|
||||||
|
var known = new JsonObject();
|
||||||
|
foreach (var raw in array.Select(n => n?.ToString()).Where(s => s != null))
|
||||||
|
{
|
||||||
|
var eq = raw.IndexOf('=');
|
||||||
|
var key = eq > 0 ? raw.Substring(0, eq) : raw;
|
||||||
|
var def = field.Of?.FirstOrDefault(f => string.Equals(f.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (def == null || eq <= 0)
|
||||||
|
unknownValues.Add(raw);
|
||||||
|
else
|
||||||
|
known[def.Key] = raw.Substring(eq + 1);
|
||||||
|
}
|
||||||
|
AddCard(known);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddEmpty()
|
||||||
|
{
|
||||||
|
AddCard(new JsonObject());
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddCard(JsonObject item)
|
||||||
|
{
|
||||||
|
if (IsValues && rows.Count > 0)
|
||||||
|
return;
|
||||||
|
var card = new PanelContainer { Name = "Card" + rows.Count };
|
||||||
|
card.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusSm, UiTokens.Well, UiTokens.LineSoft));
|
||||||
|
var column = new VBoxContainer();
|
||||||
|
card.AddChild(column);
|
||||||
|
var controls = new List<SchemaFieldControl>();
|
||||||
|
foreach (var sub in field.Of ?? new List<FieldSchema>())
|
||||||
|
{
|
||||||
|
var control = new SchemaFieldControl(column, sub, OnChanged);
|
||||||
|
control.Value = SchemaJson.ToText(Find(item, sub.Key));
|
||||||
|
controls.Add(control);
|
||||||
|
}
|
||||||
|
if (!IsValues)
|
||||||
|
{
|
||||||
|
var remove = ToolUi.Button("tools/schema/list_remove", () => Remove(card, controls), ButtonKind.Secondary);
|
||||||
|
remove.Name = "Remove";
|
||||||
|
remove.SizeFlagsHorizontal = Control.SizeFlags.ShrinkEnd;
|
||||||
|
column.AddChild(remove);
|
||||||
|
}
|
||||||
|
rows.Add(controls);
|
||||||
|
cards.AddChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonNode Find(JsonObject item, string key)
|
||||||
|
=> item.FirstOrDefault(p => string.Equals(p.Key, key, StringComparison.OrdinalIgnoreCase)).Value;
|
||||||
|
|
||||||
|
private void Remove(Control card, List<SchemaFieldControl> controls)
|
||||||
|
{
|
||||||
|
rows.Remove(controls);
|
||||||
|
cards.RemoveChild(card);
|
||||||
|
card.QueueFree();
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChanged()
|
||||||
|
{
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Clear()
|
||||||
|
{
|
||||||
|
rows.Clear();
|
||||||
|
foreach (var child in cards.GetChildren())
|
||||||
|
{
|
||||||
|
cards.RemoveChild(child);
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
namespace Cthangover.Tools.Schemas
|
||||||
|
{
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Cthangover.Tools.Common;
|
||||||
|
using Cthangover.Tools.EntityEditor;
|
||||||
|
using Cthangover.Tools.Services;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Edits a <c>locale</c> field: the locale key the JSON holds and, below it, the text of
|
||||||
|
/// that key in every language of the mod — the way the item and character editors do,
|
||||||
|
/// so a beginner types names and a professional can still rename the key. Texts go
|
||||||
|
/// through the <see cref="EntityDocument"/> and are written by the window's Save.
|
||||||
|
/// </summary>
|
||||||
|
public class LocaleFieldEditor : ISchemaFieldEditor
|
||||||
|
{
|
||||||
|
private readonly FieldSchema field;
|
||||||
|
private readonly Action changed;
|
||||||
|
private readonly LineEdit keyEdit;
|
||||||
|
private readonly VBoxContainer texts;
|
||||||
|
private readonly Dictionary<string, LineEdit> edits = new();
|
||||||
|
private EntityDocument document;
|
||||||
|
private string modId;
|
||||||
|
private bool suppress;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public FieldSchema Field => field;
|
||||||
|
|
||||||
|
/// <summary>Adds the editor for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||||
|
public LocaleFieldEditor(Container parent, FieldSchema field, Action changed)
|
||||||
|
{
|
||||||
|
this.field = field;
|
||||||
|
this.changed = changed;
|
||||||
|
keyEdit = ToolUi.Field(parent, string.IsNullOrEmpty(field.Name) ? field.Key : field.Name, ToolUi.Text(string.Empty, OnTyped), "tools/entity_editor/locale_key_tip");
|
||||||
|
keyEdit.Name = "Key_" + field.Key;
|
||||||
|
keyEdit.AddThemeFontOverride("font", ToolUi.Mono());
|
||||||
|
texts = new VBoxContainer();
|
||||||
|
parent.AddChild(texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Builds one text row per language of <paramref name="mod"/>.</summary>
|
||||||
|
public void Open(EntityDocument doc, string mod, IEnumerable<string> languages)
|
||||||
|
{
|
||||||
|
document = doc;
|
||||||
|
modId = mod;
|
||||||
|
foreach (var child in texts.GetChildren())
|
||||||
|
{
|
||||||
|
texts.RemoveChild(child);
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
edits.Clear();
|
||||||
|
foreach (var language in languages)
|
||||||
|
{
|
||||||
|
var row = new HBoxContainer();
|
||||||
|
row.AddThemeConstantOverride("separation", UiTokens.Space3);
|
||||||
|
row.AddChild(new Label { Text = string.Format(ToolUi.Tr("tools/entity_editor/text_in"), language), CustomMinimumSize = new Vector2(ToolUi.LabelWidth, 0), HorizontalAlignment = HorizontalAlignment.Right });
|
||||||
|
var edit = ToolUi.Text(string.Empty, OnTyped);
|
||||||
|
edit.Name = "Text_" + field.Key + "_" + language;
|
||||||
|
edit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||||
|
row.AddChild(edit);
|
||||||
|
texts.AddChild(row);
|
||||||
|
edits[language] = edit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows the key in <paramref name="node"/> and its texts.</summary>
|
||||||
|
public void Load(JsonNode node)
|
||||||
|
{
|
||||||
|
suppress = true;
|
||||||
|
var key = node?.ToString() ?? string.Empty;
|
||||||
|
keyEdit.Text = key;
|
||||||
|
foreach (var pair in edits)
|
||||||
|
pair.Value.Text = TextOf(pair.Key, key);
|
||||||
|
suppress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The key for the JSON (null when empty); texts are put into the document.</summary>
|
||||||
|
public JsonNode Save()
|
||||||
|
{
|
||||||
|
var key = keyEdit.Text.Trim();
|
||||||
|
if (key.Length == 0)
|
||||||
|
return null;
|
||||||
|
foreach (var pair in edits)
|
||||||
|
document?.SetText(pair.Key, key, pair.Value.Text);
|
||||||
|
return JsonValue.Create(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string TextOf(string language, string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key) || document == null)
|
||||||
|
return string.Empty;
|
||||||
|
var text = document.GetText(language, key);
|
||||||
|
if (text != null)
|
||||||
|
return text;
|
||||||
|
text = LocaleFileService.Read(modId, language, key) ?? string.Empty;
|
||||||
|
document.LoadText(language, key, text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTyped(string text)
|
||||||
|
{
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
namespace Cthangover.Tools.Schemas
|
||||||
|
{
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Cthangover.Mods.Interface.Theming;
|
||||||
|
using Cthangover.Tools.Common;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Edits a <c>map</c> field — an object whose keys are ids of <see cref="FieldSchema.Kind"/>
|
||||||
|
/// and whose values are numbers or formulas (a character's own <c>Stats</c>, an archetype's
|
||||||
|
/// growth): one row per key with a key picker, a value box and a remove button. A number
|
||||||
|
/// is written as a number, anything else as a formula text.
|
||||||
|
/// </summary>
|
||||||
|
public class MapFieldEditor : ISchemaFieldEditor
|
||||||
|
{
|
||||||
|
private readonly FieldSchema field;
|
||||||
|
private readonly Action changed;
|
||||||
|
private readonly VBoxContainer lines;
|
||||||
|
private readonly List<(OptionButton Key, List<string> Keys, LineEdit Value, Control Row)> rows = new();
|
||||||
|
private bool suppress;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public FieldSchema Field => field;
|
||||||
|
|
||||||
|
/// <summary>Adds the editor for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||||
|
public MapFieldEditor(Container parent, FieldSchema field, Action changed)
|
||||||
|
{
|
||||||
|
this.field = field;
|
||||||
|
this.changed = changed;
|
||||||
|
var box = new VBoxContainer { Name = "Map_" + field.Key };
|
||||||
|
lines = new VBoxContainer();
|
||||||
|
lines.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||||
|
box.AddChild(lines);
|
||||||
|
var add = ToolUi.Button("tools/schema/map_add", AddEmpty, ButtonKind.Secondary);
|
||||||
|
add.Name = "Add";
|
||||||
|
add.SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin;
|
||||||
|
box.AddChild(add);
|
||||||
|
ToolUi.Field(parent, string.IsNullOrEmpty(field.Name) ? field.Key : field.Name, box, field.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows the object <paramref name="node"/>.</summary>
|
||||||
|
public void Load(JsonNode node)
|
||||||
|
{
|
||||||
|
suppress = true;
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
lines.RemoveChild(row.Row);
|
||||||
|
row.Row.QueueFree();
|
||||||
|
}
|
||||||
|
rows.Clear();
|
||||||
|
if (node is JsonObject obj)
|
||||||
|
{
|
||||||
|
foreach (var pair in obj)
|
||||||
|
AddRow(pair.Key, SchemaJson.ToText(pair.Value));
|
||||||
|
}
|
||||||
|
suppress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The edited object, or null when empty.</summary>
|
||||||
|
public JsonNode Save()
|
||||||
|
{
|
||||||
|
var obj = new JsonObject();
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var key = row.Key.Selected >= 0 && row.Key.Selected < row.Keys.Count ? row.Keys[row.Key.Selected] : null;
|
||||||
|
if (!string.IsNullOrEmpty(key) && row.Value.Text.Trim().Length > 0)
|
||||||
|
obj[key] = SchemaJson.NumberOrText(row.Value.Text.Trim());
|
||||||
|
}
|
||||||
|
return obj.Count == 0 ? null : obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddEmpty()
|
||||||
|
{
|
||||||
|
AddRow(null, string.Empty);
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRow(string key, string value)
|
||||||
|
{
|
||||||
|
var row = new HBoxContainer { Name = "Row" + rows.Count };
|
||||||
|
row.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||||
|
var keys = new List<string> { string.Empty };
|
||||||
|
if (!string.IsNullOrEmpty(key) && !GameEntityIndex.Of(field.Kind).ContainsKey(key))
|
||||||
|
keys.Add(key);
|
||||||
|
keys.AddRange(GameEntityIndex.Of(field.Kind).Keys);
|
||||||
|
var picker = new OptionButton { CustomMinimumSize = new Vector2(200, 0) };
|
||||||
|
foreach (var k in keys)
|
||||||
|
picker.AddItem(k.Length == 0 ? "—" : GameEntityIndex.Label(field.Kind, k));
|
||||||
|
picker.Select(Math.Max(0, keys.FindIndex(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase))));
|
||||||
|
picker.ItemSelected += OnPicked;
|
||||||
|
row.AddChild(picker);
|
||||||
|
var edit = ToolUi.Text(field.Default ?? string.Empty, OnTyped);
|
||||||
|
edit.Text = value ?? string.Empty;
|
||||||
|
edit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||||
|
edit.AddThemeFontOverride("font", ToolUi.Mono());
|
||||||
|
row.AddChild(edit);
|
||||||
|
var entry = (picker, keys, edit, (Control)row);
|
||||||
|
row.AddChild(ToolUi.Button("tools/schema/list_remove", () => Remove(entry), ButtonKind.Secondary));
|
||||||
|
rows.Add(entry);
|
||||||
|
lines.AddChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Remove((OptionButton Key, List<string> Keys, LineEdit Value, Control Row) entry)
|
||||||
|
{
|
||||||
|
rows.Remove(entry);
|
||||||
|
lines.RemoveChild(entry.Row);
|
||||||
|
entry.Row.QueueFree();
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPicked(long index)
|
||||||
|
{
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTyped(string text)
|
||||||
|
{
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Cthangover.Tools.Schemas
|
||||||
|
{
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
/// <summary>A data editor form field with a single value: a <see cref="SchemaFieldControl"/> plus the JSON conversion.</summary>
|
||||||
|
public class ScalarFieldEditor : ISchemaFieldEditor
|
||||||
|
{
|
||||||
|
private readonly SchemaFieldControl control;
|
||||||
|
|
||||||
|
/// <summary>Adds the row for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||||
|
public ScalarFieldEditor(Container parent, FieldSchema field, Action changed)
|
||||||
|
{
|
||||||
|
control = new SchemaFieldControl(parent, field, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public FieldSchema Field => control.Field;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Load(JsonNode node) => control.Value = SchemaJson.ToText(node);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public JsonNode Save() => SchemaJson.ToNode(control.Field, control.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
namespace Cthangover.Tools.Schemas
|
||||||
|
{
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Cthangover.Mods.Interface.Theming;
|
||||||
|
using Cthangover.Tools.Common;
|
||||||
|
using Cthangover.Tools.EntityEditor;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The form control of one scalar <see cref="FieldSchema"/>, chosen by its type, as a
|
||||||
|
/// "label | editor" row. Everything reads and writes a text value, the way the DSL and
|
||||||
|
/// most JSON fields hold it, so the palette and the data editor window share these rows:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><c>bool</c> — a check box ("1" or empty);</item>
|
||||||
|
/// <item><c>choice</c>, <c>ref</c>, <c>who</c> — a drop-down of the options, of the entities of
|
||||||
|
/// the kind (by name), of player/party/characters;</item>
|
||||||
|
/// <item><c>ref_list</c> — a comma list with a drop-down that appends an entity;</item>
|
||||||
|
/// <item><c>picture</c> — a path with the picture grid of the editors;</item>
|
||||||
|
/// <item>anything else — a text box (monospace for formulas, conditions and JSON).</item>
|
||||||
|
/// </list>
|
||||||
|
/// A current value the options do not know is kept and shown first, so opening and
|
||||||
|
/// saving never loses data the schema did not foresee.
|
||||||
|
/// </summary>
|
||||||
|
public class SchemaFieldControl
|
||||||
|
{
|
||||||
|
private readonly Action changed;
|
||||||
|
private readonly LineEdit text;
|
||||||
|
private readonly OptionButton picker;
|
||||||
|
private readonly CheckBox check;
|
||||||
|
private readonly List<string> pickerValues = new();
|
||||||
|
private bool suppress;
|
||||||
|
|
||||||
|
/// <summary>Adds the row for <paramref name="field"/> to <paramref name="parent"/>; <paramref name="changed"/> runs on every edit.</summary>
|
||||||
|
public SchemaFieldControl(Container parent, FieldSchema field, Action changed)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
this.changed = changed;
|
||||||
|
var label = string.IsNullOrEmpty(field.Name) ? field.Key : field.Name;
|
||||||
|
var tip = string.IsNullOrEmpty(field.Description) ? null : field.Description;
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case "bool":
|
||||||
|
check = new CheckBox();
|
||||||
|
check.Toggled += OnToggled;
|
||||||
|
Editor = ToolUi.Field(parent, label, check, tip);
|
||||||
|
break;
|
||||||
|
case "choice":
|
||||||
|
case "ref":
|
||||||
|
case "who":
|
||||||
|
picker = new OptionButton { Name = "Picker_" + field.Key };
|
||||||
|
picker.ItemSelected += OnPicked;
|
||||||
|
Editor = ToolUi.Field(parent, label, picker, tip);
|
||||||
|
break;
|
||||||
|
case "ref_list":
|
||||||
|
case "picture":
|
||||||
|
var row = new HBoxContainer();
|
||||||
|
text = ToolUi.Text(field.Default ?? string.Empty, OnTyped);
|
||||||
|
text.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||||
|
row.AddChild(text);
|
||||||
|
if (Type == "picture")
|
||||||
|
{
|
||||||
|
var pick = UiKit.IconButton("…", ToolUi.Tr("tools/entity_editor/picture_pick_tip"));
|
||||||
|
pick.Pressed += () => OpenPicture(row);
|
||||||
|
row.AddChild(pick);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
picker = new OptionButton { TooltipText = ToolUi.Tr("tools/schema/add_ref") };
|
||||||
|
picker.ItemSelected += OnAppend;
|
||||||
|
row.AddChild(picker);
|
||||||
|
}
|
||||||
|
Editor = ToolUi.Field(parent, label, row, tip);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text = ToolUi.Text(field.Default ?? string.Empty, OnTyped);
|
||||||
|
text.Name = "Text_" + field.Key;
|
||||||
|
if (Type is "formula" or "condition" or "json")
|
||||||
|
text.AddThemeFontOverride("font", ToolUi.Mono());
|
||||||
|
Editor = ToolUi.Field(parent, label, text, tip);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
FillPicker(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The described field.</summary>
|
||||||
|
public FieldSchema Field { get; }
|
||||||
|
|
||||||
|
/// <summary>The editor control (the row's right part).</summary>
|
||||||
|
public Control Editor { get; }
|
||||||
|
|
||||||
|
/// <summary>The field type in lowercase (<c>text</c> when the schema names none).</summary>
|
||||||
|
public string Type => (Field.Type ?? "text").ToLowerInvariant();
|
||||||
|
|
||||||
|
/// <summary>The current value as text; empty when not given.</summary>
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (check != null)
|
||||||
|
return check.ButtonPressed ? "1" : string.Empty;
|
||||||
|
if (text != null)
|
||||||
|
return text.Text.Trim();
|
||||||
|
return picker != null && picker.Selected >= 0 && picker.Selected < pickerValues.Count ? pickerValues[picker.Selected] : string.Empty;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
suppress = true;
|
||||||
|
if (check != null)
|
||||||
|
check.ButtonPressed = value is "1" or "true" or "True" or "yes";
|
||||||
|
else if (text != null)
|
||||||
|
text.Text = value ?? string.Empty;
|
||||||
|
else
|
||||||
|
FillPicker(value ?? string.Empty);
|
||||||
|
suppress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FillPicker(string current)
|
||||||
|
{
|
||||||
|
if (picker == null)
|
||||||
|
return;
|
||||||
|
picker.Clear();
|
||||||
|
pickerValues.Clear();
|
||||||
|
var appendOnly = Type == "ref_list";
|
||||||
|
Add(string.Empty, appendOnly ? ToolUi.Tr("tools/schema/add_ref") : "—");
|
||||||
|
if (!appendOnly && !string.IsNullOrEmpty(current) && !Options().Contains(current, StringComparer.OrdinalIgnoreCase))
|
||||||
|
Add(current, current);
|
||||||
|
foreach (var option in Options())
|
||||||
|
Add(option, Type == "choice" ? option : GameEntityIndex.Label(KindOfOptions, option));
|
||||||
|
var index = appendOnly ? 0 : Math.Max(0, pickerValues.FindIndex(v => string.Equals(v, current, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
picker.Select(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(string value, string label)
|
||||||
|
{
|
||||||
|
pickerValues.Add(value);
|
||||||
|
picker.AddItem(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string KindOfOptions => Type == "who" ? "character" : Field.Kind;
|
||||||
|
|
||||||
|
private IEnumerable<string> Options()
|
||||||
|
{
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case "choice":
|
||||||
|
return Field.Options ?? new List<string>();
|
||||||
|
case "who":
|
||||||
|
return new[] { "player", "party" }.Concat(GameEntityIndex.Of("character").Keys);
|
||||||
|
default:
|
||||||
|
return GameEntityIndex.Of(Field.Kind).Keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTyped(string value)
|
||||||
|
{
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnToggled(bool on)
|
||||||
|
{
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPicked(long index)
|
||||||
|
{
|
||||||
|
if (!suppress)
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAppend(long index)
|
||||||
|
{
|
||||||
|
if (suppress || index <= 0 || index >= pickerValues.Count)
|
||||||
|
return;
|
||||||
|
var items = text.Text.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||||
|
if (!items.Contains(pickerValues[(int)index]))
|
||||||
|
items.Add(pickerValues[(int)index]);
|
||||||
|
text.Text = string.Join(",", items);
|
||||||
|
picker.Select(0);
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenPicture(Node host)
|
||||||
|
{
|
||||||
|
var dialog = new PicturePickerDialog(Field.Group ?? "items", text.Text);
|
||||||
|
dialog.Picked += OnPicturePicked;
|
||||||
|
host.AddChild(dialog);
|
||||||
|
dialog.PopupCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPicturePicked(string id)
|
||||||
|
{
|
||||||
|
text.Text = id;
|
||||||
|
changed?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Cthangover.Tools.Schemas
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Cthangover.Mods.Schemas;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Picks the editor of a schema field by its type: a card list for <c>list</c> and
|
||||||
|
/// <c>values</c>, a key/value table for <c>map</c>, texts per language for <c>locale</c>,
|
||||||
|
/// a single control for everything else. Shared by the data editors (their "fields of
|
||||||
|
/// other mods" block) and the Data Editor window, so a field looks the same in both.
|
||||||
|
/// </summary>
|
||||||
|
public static class SchemaFieldEditors
|
||||||
|
{
|
||||||
|
/// <summary>Adds the editor of <paramref name="field"/> to <paramref name="parent"/>; <paramref name="changed"/> runs on every edit.</summary>
|
||||||
|
public static ISchemaFieldEditor Create(Container parent, FieldSchema field, Action changed)
|
||||||
|
{
|
||||||
|
switch (field.Type?.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "list":
|
||||||
|
case "values":
|
||||||
|
return new ListFieldEditor(parent, field, changed);
|
||||||
|
case "map":
|
||||||
|
return new MapFieldEditor(parent, field, changed);
|
||||||
|
case "locale":
|
||||||
|
return new LocaleFieldEditor(parent, field, changed);
|
||||||
|
default:
|
||||||
|
return new ScalarFieldEditor(parent, field, changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user