diff --git a/docs/design/defs.md b/docs/design/defs.md index b4c0317..7fdd9de 100644 --- a/docs/design/defs.md +++ b/docs/design/defs.md @@ -218,6 +218,10 @@ create раскладка замораживается вместе с набо Моды — подпапки на диске сервера, например `mods//`. Ядро — мод `core`: тот же формат папки, но **всегда включён**, в UI создания его нельзя снять. Остальные папки — чекбоксы. +Живой образец — [`../../src/HSchool.Server/mods/example/`](../../src/HSchool.Server/mods/example/): +`pack.jsonc`, две черты, набор имён, патч, last-wins и одна комната. Пока его не выбрали, ваниль +не меняется. README внутри папки — как добавить своё. + ``` mods/ core/ @@ -227,7 +231,7 @@ mods/ en.jsonc maps/ default.jsonc # ванильная раскладка - furniture-pack/ + example/ # образец, не контент; выключается в create defs/ patches/ # правки чужих def, не копии localizations/ @@ -284,6 +288,7 @@ mods/ | Пустая комната | Можно; граф связный, двор всегда в графе | | Действия | Заготовки в def, без исполнения и без приказов | | Моды | `mods//`; `core` всегда первый и не снимается; остальные — UI create | +| Образец пака | `mods/example/`: настоящая папка рядом с `core`, не контент | | Коллизии имён | Последний пакет победил (defs и локали), warning в лог | | Наследование | `parent`, `abstract`; поле ребёнка заменяет, не мержит массивы | | Патчи | `patches/` после наследования; ops: add / replace / remove; неизвестный op — ошибка | diff --git a/docs/design/foundation.md b/docs/design/foundation.md index 19b5564..9c5a543 100644 --- a/docs/design/foundation.md +++ b/docs/design/foundation.md @@ -12,8 +12,9 @@ **Моды существуют на бумаге — остался проверяемый путь.** Удостоверение пака уже есть: `pack.jsonc` (версия и `requires`), название в локалях по id, `GET /api/mods?lang=` отдаёт подпись, создание отказывает во внятном коде, если зависимости нет или они замкнуты в цикл. -Порядок загрузки сервер выстраивает сам и кладёт в сейв. В `mods/` по-прежнему лежит один -`core`: last-wins, патчи и дорога «игрок выбрал мод» ждут настоящую папку — это фаза 23. +Порядок загрузки сервер выстраивает сам и кладёт в сейв. Рядом с `core` лежит `mods/example` — +маленький скучный пак на диске, чтобы last-wins, патчи и дорога «игрок выбрал мод» проверялись +настоящей папкой, а не документами в памяти. **Числа поведения живут в коде вопреки [`ai.md`](ai.md).** Там записано: «Числа поведения — отдельный деф правил, как `StaffingDef` у штата». В `BehaviorDef` уехали порог нужды, скорость @@ -66,7 +67,7 @@ проверена ни разу. Пак должен быть виден из тестов хоста, то есть лежать рядом с `core` в `mods/` и ехать в вывод -сборки теми же правилами. +сборки теми же правилами. Живая папка — `src/HSchool.Server/mods/example/`. ## Числа поведения — в `BehaviorDef` diff --git a/docs/phases/23-example-pack.md b/docs/phases/23-example-pack.md index 83dc268..3cc5753 100644 --- a/docs/phases/23-example-pack.md +++ b/docs/phases/23-example-pack.md @@ -12,21 +12,21 @@ ## Задачи -- [ ] Пак `mods/example` рядом с `core`: `pack.jsonc`, свои локали, пара черт, набор имён, +- [x] Пак `mods/example` рядом с `core`: `pack.jsonc`, свои локали, пара черт, набор имён, патч чужого def и одна комната -- [ ] Пак нарочно скучный: он образец и фикстура, а не контент. Ванильную игру он не меняет, +- [x] Пак нарочно скучный: он образец и фикстура, а не контент. Ванильную игру он не меняет, пока не выбран -- [ ] Пак едет в вывод сборки тестов теми же правилами, что и `core` -- [ ] Короткий `README.md` внутри пака: что где лежит и как добавить своё -- [ ] Ориентир в [`../design/defs.md`](../design/defs.md) показывает на него как на образец +- [x] Пак едет в вывод сборки тестов теми же правилами, что и `core` +- [x] Короткий `README.md` внутри пака: что где лежит и как добавить своё +- [x] Ориентир в [`../design/defs.md`](../design/defs.md) показывает на него как на образец ## Тесты, без которых фаза не закрыта -- [ ] `GET /api/catalog?mods=example` отдаёт типы и подписи пака поверх `core` -- [ ] Школа создаётся с паком и поднимается с ним после перезапуска -- [ ] Патч пака виден в каталоге школы, а без пака его нет -- [ ] Одинаковый `defName` в `core` и в паке — побеждает пак, в логе предупреждение -- [ ] Карта пака проходит валидацию и годится для создания школы +- [x] `GET /api/catalog?mods=example` отдаёт типы и подписи пака поверх `core` +- [x] Школа создаётся с паком и поднимается с ним после перезапуска +- [x] Патч пака виден в каталоге школы, а без пака его нет +- [x] Одинаковый `defName` в `core` и в паке — побеждает пак, в логе предупреждение +- [x] Карта пака проходит валидацию и годится для создания школы ## Критерий готовности diff --git a/docs/protocol.md b/docs/protocol.md index a451b74..eeb1521 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -69,6 +69,9 @@ still a pack: the id stands in for the name, `version` is empty, `requires` is e { "mods": [{ "id": "core", "required": true, "label": "Базовая игра", "version": "1.0", "requires": [] }] } ``` +The repo also ships `example` next to `core` — a sample pack, not required. It appears in this +list with `required: false` and `requires: ["core"]`. Vanilla create omits it. + ### `GET /api/catalog?lang=ru|en&mods=addon1,addon2` Placeable (non-abstract) types plus labels in `lang`, and the last-wins `maps/default.jsonc` for diff --git a/src/HSchool.Server/mods/example/README.md b/src/HSchool.Server/mods/example/README.md new file mode 100644 index 0000000..8c3e46e --- /dev/null +++ b/src/HSchool.Server/mods/example/README.md @@ -0,0 +1,36 @@ +# Example pack + +A tiny fixture, not content. It sits next to `core` and is **off until the player ticks it**. +Vanilla play does not change without that tick. + +## Layout + +``` +example/ + pack.jsonc # version + requires: ["core"] + localizations/{ru,en}.jsonc + defs/traits/traits.jsonc # two traits + defs/namesets/example.jsonc + defs/rooms/store.jsonc # one placeable room + defs/things/chair.jsonc # last-wins: same defName as core + patches/principals-office.jsonc + README.md # this file +``` + +The pack name is the `example` key in the locale files, not a field in `pack.jsonc`. + +## How to add something + +1. New type: a JSONC file under `defs//`. The folder chooses the kind (`traits`, `rooms`, …); + the object needs a `defName`. +2. Label: the same `defName` (or the pack id) in both locale files. Missing keys load, but the UI + shows the raw id and the loader warns. +3. Edit a core type without copying it: a file in `patches/` with `target` and `add` / `replace` / + `remove`. Patches run after inheritance, in pack order. +4. Same `defName` as `core` (or an earlier pack): the later file wins and the log warns. That is + what `defs/things/chair.jsonc` is for — a sample of last-wins, not a better chair. +5. Default map: only `maps/default.jsonc` replaces the layout, last file wins. This pack does not + ship one, so a school with the pack still uses core's yard. + +There is no `maps/` folder here on purpose. A layout that uses `ExampleStore` is a create-dialog +map, not a second default school. diff --git a/src/HSchool.Server/mods/example/defs/namesets/example.jsonc b/src/HSchool.Server/mods/example/defs/namesets/example.jsonc new file mode 100644 index 0000000..8426802 --- /dev/null +++ b/src/HSchool.Server/mods/example/defs/namesets/example.jsonc @@ -0,0 +1,17 @@ +{ + "defName": "ExampleNames", + "patronymicRule": "slavic", + "defaultGivenDeclension": "hard", + "defaultSurnameDeclension": "ov", + "nativeLanguages": ["RussianLanguage"], + "relatedLanguageChance": 0, + "maleGiven": [ + { "form": "Иван" }, + ], + "femaleGiven": [ + { "form": "Анна", "declension": "a" }, + ], + "surnames": [ + { "male": "Тестов", "female": "Тестова" }, + ], +} diff --git a/src/HSchool.Server/mods/example/defs/rooms/store.jsonc b/src/HSchool.Server/mods/example/defs/rooms/store.jsonc new file mode 100644 index 0000000..f5f27c4 --- /dev/null +++ b/src/HSchool.Server/mods/example/defs/rooms/store.jsonc @@ -0,0 +1,7 @@ +{ + "defName": "ExampleStore", + "slots": [ + { "key": "lockers", "thing": "Locker", "count": 4 }, + ], + "travelMinutes": 0.5, +} diff --git a/src/HSchool.Server/mods/example/defs/things/chair.jsonc b/src/HSchool.Server/mods/example/defs/things/chair.jsonc new file mode 100644 index 0000000..da89bb0 --- /dev/null +++ b/src/HSchool.Server/mods/example/defs/things/chair.jsonc @@ -0,0 +1,2 @@ +// Same defName as core: the last pack wins and the loader warns. Chat is already a core action. +{ "defName": "Chair", "actions": ["Sit", "Chat"] } diff --git a/src/HSchool.Server/mods/example/defs/traits/traits.jsonc b/src/HSchool.Server/mods/example/defs/traits/traits.jsonc new file mode 100644 index 0000000..be946ac --- /dev/null +++ b/src/HSchool.Server/mods/example/defs/traits/traits.jsonc @@ -0,0 +1,14 @@ +[ + { + "defName": "ExampleEarlyRiser", + "weight": 1, + "incompatible": ["ExampleNightOwl"], + "commuteMinutes": 2, + }, + { + "defName": "ExampleNightOwl", + "weight": 1, + "incompatible": ["ExampleEarlyRiser"], + "commuteMinutes": -2, + }, +] diff --git a/src/HSchool.Server/mods/example/localizations/en.jsonc b/src/HSchool.Server/mods/example/localizations/en.jsonc new file mode 100644 index 0000000..4aeba80 --- /dev/null +++ b/src/HSchool.Server/mods/example/localizations/en.jsonc @@ -0,0 +1,7 @@ +{ + "example": "Example pack", + "ExampleEarlyRiser": "Early riser", + "ExampleNightOwl": "Night owl", + "ExampleNames": "Example names", + "ExampleStore": "Storeroom", +} diff --git a/src/HSchool.Server/mods/example/localizations/ru.jsonc b/src/HSchool.Server/mods/example/localizations/ru.jsonc new file mode 100644 index 0000000..d34c755 --- /dev/null +++ b/src/HSchool.Server/mods/example/localizations/ru.jsonc @@ -0,0 +1,7 @@ +{ + "example": "Пример мода", + "ExampleEarlyRiser": "Ранняя пташка", + "ExampleNightOwl": "Сова", + "ExampleNames": "Примерные имена", + "ExampleStore": "Кладовая", +} diff --git a/src/HSchool.Server/mods/example/pack.jsonc b/src/HSchool.Server/mods/example/pack.jsonc new file mode 100644 index 0000000..3c6142e --- /dev/null +++ b/src/HSchool.Server/mods/example/pack.jsonc @@ -0,0 +1,4 @@ +{ + "version": "1.0", + "requires": ["core"], +} diff --git a/src/HSchool.Server/mods/example/patches/principals-office.jsonc b/src/HSchool.Server/mods/example/patches/principals-office.jsonc new file mode 100644 index 0000000..981b6ca --- /dev/null +++ b/src/HSchool.Server/mods/example/patches/principals-office.jsonc @@ -0,0 +1,6 @@ +{ + "target": "PrincipalsOffice", + "ops": [ + { "op": "add", "path": "/slots/-", "value": { "key": "exampleLocker", "thing": "Locker" } }, + ], +} diff --git a/tests/HSchool.Ai.Tests/HSchool.Ai.Tests.csproj b/tests/HSchool.Ai.Tests/HSchool.Ai.Tests.csproj index d8bef11..73a4e24 100644 --- a/tests/HSchool.Ai.Tests/HSchool.Ai.Tests.csproj +++ b/tests/HSchool.Ai.Tests/HSchool.Ai.Tests.csproj @@ -28,6 +28,10 @@ vanilla\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest + + example\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + diff --git a/tests/HSchool.AppHost.Tests/ExamplePackTests.cs b/tests/HSchool.AppHost.Tests/ExamplePackTests.cs new file mode 100644 index 0000000..740e187 --- /dev/null +++ b/tests/HSchool.AppHost.Tests/ExamplePackTests.cs @@ -0,0 +1,165 @@ +using System.Net.Http.Json; + +namespace HSchool.AppHost.Tests; + +/// +/// The shipped mods/example folder: catalog, create, reload and a map that uses its room. +/// Temp packs in other tests stay disposable; this one has to survive a restart. +/// +[Collection(AppHostCollection.Name)] +public class ExamplePackTests(AppHostFixture fixture) +{ + private static readonly DateTime ExpectedDefaultStart = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc); + + [Fact] + public async Task Catalog_WithExample_AddsPackTypesAndLabelsOnTopOfCore() + { + using var client = fixture.App.CreateHttpClient("server"); + + var ru = await client.GetFromJsonAsync( + "/api/catalog?lang=ru&mods=example", + TestContext.Current.CancellationToken); + var en = await client.GetFromJsonAsync( + "/api/catalog?lang=en&mods=example", + TestContext.Current.CancellationToken); + + Assert.NotNull(ru); + Assert.NotNull(en); + Assert.Equal("Кабинет директора", Assert.Single(ru.Rooms, room => room.DefName == "PrincipalsOffice").Label); + Assert.Equal("Кладовая", Assert.Single(ru.Rooms, room => room.DefName == "ExampleStore").Label); + Assert.Equal("Storeroom", Assert.Single(en.Rooms, room => room.DefName == "ExampleStore").Label); + Assert.Equal("Примерные имена", Assert.Single(ru.NameSets, set => set.DefName == "ExampleNames").Label); + Assert.Equal("Example names", Assert.Single(en.NameSets, set => set.DefName == "ExampleNames").Label); + Assert.Equal("yard", ru.DefaultMap.Territory?.Id); + } + + [Fact] + public async Task Catalog_PackPatch_IsVisibleOnlyWhenThePackIsSelected() + { + using var client = fixture.App.CreateHttpClient("server"); + + var vanilla = await client.GetFromJsonAsync("/api/catalog?lang=ru", TestContext.Current.CancellationToken); + var withPack = await client.GetFromJsonAsync( + "/api/catalog?lang=ru&mods=example", + TestContext.Current.CancellationToken); + + Assert.NotNull(vanilla); + Assert.NotNull(withPack); + Assert.DoesNotContain( + Assert.Single(vanilla.Rooms, room => room.DefName == "PrincipalsOffice").Slots, + slot => slot.Key == "exampleLocker"); + Assert.Contains( + Assert.Single(withPack.Rooms, room => room.DefName == "PrincipalsOffice").Slots, + slot => slot.Key == "exampleLocker"); + Assert.DoesNotContain(vanilla.Rooms, room => room.DefName == "ExampleStore"); + } + + [Fact] + public async Task CreateSchool_WithExample_SurvivesReload() + { + using var client = fixture.App.CreateHttpClient("server"); + await SchoolApiTests.ResetAsync(client); + + var created = await CreateWithModsAsync(client, "С примером", ["example"]); + Assert.Equal(["core", "example"], created.ModIds); + + using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); + reload.EnsureSuccessStatusCode(); + + var restored = (await SchoolApiTests.GetSchoolsAsync(client)).Schools.Single(school => school.Id == created.Id); + Assert.Equal(created.Name, restored.Name); + Assert.Equal(["core", "example"], restored.ModIds); + Assert.True(restored.Running); + } + + [Fact] + public async Task CreateSchool_WithThePackRoomOnTheMap_Succeeds() + { + using var client = fixture.App.CreateHttpClient("server"); + await SchoolApiTests.ResetAsync(client); + + using var response = await client.PostAsJsonAsync( + "/api/schools", + new + { + name = "Кладовая во дворе", + startDate = ExpectedDefaultStart, + modIds = new[] { "example" }, + map = PackRoomMap, + }, + TestContext.Current.CancellationToken); + response.EnsureSuccessStatusCode(); + + var created = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); + Assert.NotNull(created); + Assert.Equal(["core", "example"], created.ModIds); + } + + [Fact] + public async Task CreateSchool_WithoutThePack_StaysVanilla() + { + using var client = fixture.App.CreateHttpClient("server"); + await SchoolApiTests.ResetAsync(client); + + var created = await SchoolApiTests.CreateAsync(client, "Без примера", ExpectedDefaultStart); + Assert.Equal(["core"], created.ModIds); + } + + private static async Task CreateWithModsAsync( + HttpClient client, + string name, + string[] modIds) + { + using var response = await client.PostAsJsonAsync( + "/api/schools", + new { name, startDate = ExpectedDefaultStart, modIds }, + TestContext.Current.CancellationToken); + response.EnsureSuccessStatusCode(); + + var created = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); + Assert.NotNull(created); + return created; + } + + private static readonly object PackRoomMap = new + { + territory = new { id = "yard", def = "SchoolYard" }, + buildings = new[] { new { id = "main", def = "MainBuilding" } }, + floors = new[] { new { id = "floor-1", def = "StandardFloor", building = "main", label = "1" } }, + rooms = new[] + { + new { id = "store", def = "ExampleStore", building = "main", floor = "floor-1", slots = Array.Empty() }, + }, + links = new[] { new { a = "yard", b = "store" } }, + }; + + private sealed record CatalogResponse( + IReadOnlyList Territories, + IReadOnlyList Buildings, + IReadOnlyList Floors, + IReadOnlyList Rooms, + IReadOnlyList Things, + IReadOnlyList NameSets, + DayFrameResponse? DayFrame, + IReadOnlyList Holidays, + MapLayoutResponse DefaultMap); + + private sealed record DefInfoResponse(string DefName, string Label); + + private sealed record NameSetInfoResponse(string DefName, string Label); + + private sealed record RoomInfoResponse( + string DefName, + string Label, + IReadOnlyList Slots); + + private sealed record RoomSlotResponse(string Key, string Thing); + + private sealed record DayFrameResponse(string DefName, string Label); + + private sealed record HolidayInfoResponse(string DefName, string Label); + + private sealed record MapLayoutResponse(TerritoryResponse? Territory); + + private sealed record TerritoryResponse(string Id, string Def); +} diff --git a/tests/HSchool.Content.Tests/ExamplePackTests.cs b/tests/HSchool.Content.Tests/ExamplePackTests.cs new file mode 100644 index 0000000..2c1bbb8 --- /dev/null +++ b/tests/HSchool.Content.Tests/ExamplePackTests.cs @@ -0,0 +1,97 @@ +namespace HSchool.Content.Tests; + +/// +/// The shipped mods/example folder, copied next to vanilla core. Last-wins, patches and a +/// placeable room have to work from disk — in-memory documents already have their own tests. +/// +public class ExamplePackTests +{ + private const string ExamplePackId = "example"; + + private readonly CatalogLoader _loader = new(); + + [Fact] + public void DuplicateChair_LastWinsAndWarns() + { + var log = new RecordingLog(); + var catalog = LoadCoreAndExample(log); + + Assert.Equal(["Sit", "Chat"], catalog.Things["Chair"].Actions); + Assert.Contains( + log.Warnings, + warning => warning.Contains("Chair", StringComparison.Ordinal) + && warning.Contains(ExamplePackId, StringComparison.Ordinal)); + } + + [Fact] + public void PatchAddsASlot_OnlyWhenThePackIsLoaded() + { + var vanilla = LoadCore(); + Assert.DoesNotContain(vanilla.Rooms["PrincipalsOffice"].Slots, slot => slot.Key == "exampleLocker"); + + var withPack = LoadCoreAndExample(); + Assert.Contains(withPack.Rooms["PrincipalsOffice"].Slots, slot => slot.Key == "exampleLocker" && slot.Thing == "Locker"); + } + + [Fact] + public void PackTypes_LoadWithLocalizedLabels() + { + var catalog = LoadCoreAndExample(); + + Assert.True(catalog.Traits.ContainsKey("ExampleEarlyRiser")); + Assert.True(catalog.Traits.ContainsKey("ExampleNightOwl")); + Assert.True(catalog.NameSets.ContainsKey("ExampleNames")); + Assert.True(catalog.Rooms.ContainsKey("ExampleStore")); + Assert.Equal("Кладовая", catalog.Label("ru", catalog.Rooms["ExampleStore"])); + Assert.Equal("Storeroom", catalog.Label("en", catalog.Rooms["ExampleStore"])); + Assert.Equal("Примерные имена", catalog.Label("ru", catalog.NameSets["ExampleNames"])); + Assert.Equal(["RussianLanguage"], catalog.NameSets["ExampleNames"].Spoken.ToArray()); + Assert.True(catalog.Rooms.ContainsKey("PrincipalsOffice")); + Assert.Equal("Кабинет директора", catalog.Label("ru", catalog.Rooms["PrincipalsOffice"])); + } + + [Fact] + public void MapUsingThePackRoom_PassesValidation() + { + var catalog = LoadCoreAndExample(); + var map = new MapLayout + { + Territory = new TerritoryNode { Id = "yard", Def = "SchoolYard" }, + Buildings = [new BuildingNode { Id = "main", Def = "MainBuilding" }], + Floors = [new FloorNode { Id = "floor-1", Def = "StandardFloor", Building = "main" }], + Rooms = + [ + new RoomNode { Id = "store", Def = "ExampleStore", Building = "main", Floor = "floor-1" }, + ], + Links = [new MapLink { A = "yard", B = "store" }], + }; + + MapValidator.Validate(map, catalog); + var defaults = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId, ExamplePackId], Documents()); + Assert.NotNull(defaults); + Assert.Equal("SchoolYard", defaults.Territory?.Def); + } + + private DefCatalog LoadCore() => + _loader.Load( + [CatalogLoader.CorePackId], + PackDocuments.FromDirectory(CatalogLoader.CorePackId, CoreRoot())); + + private DefCatalog LoadCoreAndExample(IContentLog? log = null) => + _loader.Load([CatalogLoader.CorePackId, ExamplePackId], Documents(), log); + + private static IReadOnlyList Documents() + { + var example = ExampleRoot(); + Assert.True(Directory.Exists(example), $"Example pack was not copied to {example}."); + return + [ + .. PackDocuments.FromDirectory(CatalogLoader.CorePackId, CoreRoot()), + .. PackDocuments.FromDirectory(ExamplePackId, example), + ]; + } + + private static string CoreRoot() => Path.Combine(AppContext.BaseDirectory, "vanilla"); + + private static string ExampleRoot() => Path.Combine(AppContext.BaseDirectory, "example"); +} diff --git a/tests/HSchool.Content.Tests/HSchool.Content.Tests.csproj b/tests/HSchool.Content.Tests/HSchool.Content.Tests.csproj index a9d4b70..0efb927 100644 --- a/tests/HSchool.Content.Tests/HSchool.Content.Tests.csproj +++ b/tests/HSchool.Content.Tests/HSchool.Content.Tests.csproj @@ -25,6 +25,10 @@ vanilla\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest + + example\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + diff --git a/tests/HSchool.People.Tests/HSchool.People.Tests.csproj b/tests/HSchool.People.Tests/HSchool.People.Tests.csproj index 06c812b..4bef833 100644 --- a/tests/HSchool.People.Tests/HSchool.People.Tests.csproj +++ b/tests/HSchool.People.Tests/HSchool.People.Tests.csproj @@ -26,6 +26,10 @@ vanilla\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest + + example\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + PreserveNewest diff --git a/tests/HSchool.Schedule.Tests/HSchool.Schedule.Tests.csproj b/tests/HSchool.Schedule.Tests/HSchool.Schedule.Tests.csproj index fd0ad00..8f96e43 100644 --- a/tests/HSchool.Schedule.Tests/HSchool.Schedule.Tests.csproj +++ b/tests/HSchool.Schedule.Tests/HSchool.Schedule.Tests.csproj @@ -26,6 +26,10 @@ vanilla\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest + + example\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + diff --git a/tests/HSchool.Simulation.Tests/HSchool.Simulation.Tests.csproj b/tests/HSchool.Simulation.Tests/HSchool.Simulation.Tests.csproj index d270cf9..7a104a2 100644 --- a/tests/HSchool.Simulation.Tests/HSchool.Simulation.Tests.csproj +++ b/tests/HSchool.Simulation.Tests/HSchool.Simulation.Tests.csproj @@ -29,6 +29,10 @@ vanilla\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest + + example\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest +