Data editor base: EntityEditorWindow (searchable list, form over JSON entities, texts in every language of the mod, other fields as JSON), EntityDocument, LocaleFileService, PropertiesText; mod tests

This commit is contained in:
cthangover committed 2026-10-23 19:09:39 +02:00
1 parent 6ca1fff35f
commit e1a1d770cf
14 files changed
+1502 -1

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
namespace Cthangover.Tools.EntityEditor.Tests
{
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Cthangover.Core.Mods.Testing;
/// <summary>The data editors' document: files of one kind, add/copy/delete, texts, undo and what gets written.</summary>
public class EntityDocumentTests
{
private const string Items = "{\n \"Kind\": \"item\",\n \"Items\": [\n {\n \"Id\": \"food/bread\",\n \"Name\": \"items/food/bread/name\",\n \"Cost\": 2,\n \"heal\": 5\n }\n ]\n}\n";
private static EntityDocument Load(out List<string> skipped) => EntityDocument.Load("item", "items/my_mod.json", new[]
{
("items/food/bread.json", Items),
("characters/hero.json", "{\"Kind\": \"character\", \"Items\": [{\"Id\": \"hero\"}]}"),
("items/broken.json", "{\"Kind\": \"item\", \"Items\": [ {"),
("scenes/town.json", "{\"Name\": \"town\"}")
}, out skipped);
[ModTest("entity_document/load_takes_only_its_kind")]
static void LoadTakesOnlyItsKind(ModTestContext ctx)
{
var document = Load(out var skipped);
ModAssert.Equal(1, document.Records.Count, "only the item file");
ModAssert.Equal("food/bread", document.Records[0].Id, "its entity");
ModAssert.Equal("items/food/bread.json", document.Records[0].File, "its file");
ModAssert.Equal(1, skipped.Count, "the broken file of this kind is reported");
ModAssert.Equal("items/broken.json", skipped[0], "by path");
ModAssert.Equal(0, document.ChangedFiles().Count, "nothing to write after loading");
}
[ModTest("entity_document/unknown_fields_survive")]
static void UnknownFieldsSurvive(ModTestContext ctx)
{
var document = Load(out _);
document.Records[0].Data["Cost"] = 3;
var changed = document.ChangedFiles();
ModAssert.Equal(1, changed.Count, "the changed file");
var root = JsonNode.Parse(changed[0].Text).AsObject();
var item = root["Items"][0].AsObject();
ModAssert.Equal("3", item["Cost"].ToString(), "the edit");
ModAssert.Equal("5", item["heal"].ToString(), "another mod's field is kept");
ModAssert.Equal("item", root["Kind"].ToString(), "the kind is kept");
ModAssert.True(changed[0].Text.EndsWith("}\n"), "LF line break at the end");
ModAssert.Equal(Items.Replace("\"Cost\": 2", "\"Cost\": 3"), changed[0].Text, "the rest of the file is byte for byte the same");
}
[ModTest("entity_document/add_copy_delete")]
static void AddCopyDelete(ModTestContext ctx)
{
var document = Load(out _);
var added = document.Add(new JsonObject { ["Id"] = document.FreeId("food/bread") });
ModAssert.Equal("food/bread_2", added.Id, "a taken id gets a number");
ModAssert.Equal("items/my_mod.json", added.File, "new entries go to the new file");
var copy = document.Duplicate(document.Find("food/bread"), document.FreeId("food/bread"));
ModAssert.Equal("food/bread_3", copy.Id, "the copy gets a free id");
ModAssert.Equal("items/food/bread.json", copy.File, "and stays in the original's file");
ModAssert.Equal("5", copy.Data["heal"].ToString(), "with all fields");
ModAssert.Equal("food/bread", document.Records[0].Id, "the original first");
ModAssert.Equal("food/bread_3", document.Records[1].Id, "the copy right after it");
document.Remove(document.Find("food/bread"));
ModAssert.Null(document.Find("food/bread"), "removed");
var files = document.ChangedFiles().Select(f => f.Path).OrderBy(p => p).ToList();
ModAssert.Equal(2, files.Count, "both files are written");
ModAssert.Equal("items/food/bread.json", files[0], "the changed one");
ModAssert.Equal("items/my_mod.json", files[1], "the new one");
ModAssert.True(document.ChangedFiles().First(f => f.Path == "items/my_mod.json").Text.Contains("\"Kind\": \"item\""), "a new file declares the kind");
}
[ModTest("entity_document/undo_restores_data_and_texts")]
static void UndoRestoresDataAndTexts(ModTestContext ctx)
{
var document = Load(out _);
document.LoadText("ru", "items/food/bread/name", "Хлеб");
document.BeginChange();
document.Records[0].Data["Cost"] = 9;
document.SetText("ru", "items/food/bread/name", "Каравай");
document.BeginChange();
document.Add(new JsonObject { ["Id"] = "food/cake" });
ModAssert.True(document.Undo(), "undo the add");
ModAssert.Null(document.Find("food/cake"), "the new entry is gone");
ModAssert.True(document.Undo(), "undo the edit");
ModAssert.Equal("2", document.Find("food/bread").Data["Cost"].ToString(), "the cost is back");
ModAssert.Equal("Хлеб", document.GetText("ru", "items/food/bread/name"), "the text is back");
ModAssert.False(document.Undo(), "nothing more");
ModAssert.Equal(0, document.ChangedFiles().Count + document.ChangedTexts().Count, "back to what is on disk");
ModAssert.True(document.Redo(), "redo the edit");
var texts = document.ChangedTexts();
ModAssert.Equal(1, texts.Count, "one changed text");
ModAssert.Equal("ru", texts[0].Language, "its language");
ModAssert.Equal("items/food/bread/name", texts[0].Key, "its key");
ModAssert.Equal("Каравай", texts[0].Value, "its value");
document.MarkSaved();
ModAssert.Equal(0, document.ChangedFiles().Count + document.ChangedTexts().Count, "nothing left after saving");
}
[ModTest("entity_document/validate_reports_ids")]
static void ValidateReportsIds(ModTestContext ctx)
{
var document = Load(out _);
ModAssert.Equal(0, document.Validate().Count, "a loaded mod is valid");
document.Add(new JsonObject { ["Id"] = "FOOD/BREAD" });
document.Add(new JsonObject { ["Name"] = "x" });
var problems = document.Validate();
ModAssert.Equal(2, problems.Count, "an empty id and a duplicate");
ModAssert.Equal("tools/entity_editor/problem_id", problems[0].Key, "empty id first");
ModAssert.Equal("tools/entity_editor/problem_duplicate", problems[1].Key, "a duplicate ignores case");
}
}
}
+32
View File
@@ -0,0 +1,32 @@
namespace Cthangover.Tools.Services.Tests
{
using Cthangover.Core.Mods.Testing;
/// <summary>Changing one key of a .properties text without touching the rest.</summary>
public class PropertiesTextTests
{
private const string Text = "# items\nitems/bread/name = Хлеб\nitems/bread/desc=Свежий\n\nitems/cake/name = Торт\n";
[ModTest("properties_text/get_reads_like_the_loader")]
static void GetReadsLikeTheLoader(ModTestContext ctx)
{
ModAssert.Equal("Хлеб", PropertiesText.Get(Text, "items/bread/name"), "spaces around '=' are trimmed");
ModAssert.Equal("Свежий", PropertiesText.Get(Text, "items/bread/desc"), "no spaces work too");
ModAssert.Null(PropertiesText.Get(Text, "items/pie/name"), "a missing key");
ModAssert.Null(PropertiesText.Get(null, "x"), "no text");
ModAssert.Equal("b", PropertiesText.Get("x = a\nx = b\n", "x"), "the last line wins");
ModAssert.Equal(3, PropertiesText.Keys(Text).Count, "three keys, the comment is not one");
}
[ModTest("properties_text/set_changes_one_line")]
static void SetChangesOneLine(ModTestContext ctx)
{
ModAssert.Equal(Text.Replace("= Хлеб", "= Булка"), PropertiesText.Set(Text, "items/bread/name", "Булка"), "only that line changes");
ModAssert.Equal(Text + "items/pie/name = Пирог\n", PropertiesText.Set(Text, "items/pie/name", "Пирог"), "a new key is appended");
ModAssert.Equal("a = 1\nb = 2\n", PropertiesText.Set("a = 1", "b", "2"), "a missing final line break is added first");
ModAssert.Equal("a = 1\r\nb = 2\r\n", PropertiesText.Set("a = 1\r\n", "b", "2"), "CRLF text stays CRLF");
ModAssert.Equal("a = x\r\n", PropertiesText.Set("a = 1\r\n", "a", "x"), "a CRLF line is rewritten with its CR");
ModAssert.Equal("a = one two\n", PropertiesText.Set("", "a", "one\ntwo"), "a line break in a value becomes a space");
}
}
}