Merge phase 23 example pack.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+6
-1
@@ -218,6 +218,10 @@ create раскладка замораживается вместе с набо
|
||||
Моды — подпапки на диске сервера, например `mods/<id>/`. Ядро — мод `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/<id>/`; `core` всегда первый и не снимается; остальные — UI create |
|
||||
| Образец пака | `mods/example/`: настоящая папка рядом с `core`, не контент |
|
||||
| Коллизии имён | Последний пакет победил (defs и локали), warning в лог |
|
||||
| Наследование | `parent`, `abstract`; поле ребёнка заменяет, не мержит массивы |
|
||||
| Патчи | `patches/` после наследования; ops: add / replace / remove; неизвестный op — ошибка |
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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] Карта пака проходит валидацию и годится для создания школы
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
| Фаза | Статус | Зачем |
|
||||
| --- | --- | --- |
|
||||
| [22. Удостоверение пака](22-mod-identity.md) | ✅ | Название, версия, зависимости и порядок загрузки |
|
||||
| [23. Пример мода](23-example-pack.md) | 🔄 | Настоящая папка вместо документов в памяти |
|
||||
| [23. Пример мода](23-example-pack.md) | ✅ | Настоящая папка вместо документов в памяти |
|
||||
| [24. Числа поведения](24-behavior-numbers.md) | ✅ | Веса целей в `BehaviorDef`, а не в коде |
|
||||
| [25. Золотые файлы](25-golden-fixtures.md) | ✅ | Отпечаток ростера, старый сейв, кривая голода, нехватка учителей |
|
||||
| [26. Свой сид](26-school-seed.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
|
||||
|
||||
@@ -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/<kind>/`. 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.
|
||||
@@ -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": "Тестова" },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"defName": "ExampleStore",
|
||||
"slots": [
|
||||
{ "key": "lockers", "thing": "Locker", "count": 4 },
|
||||
],
|
||||
"travelMinutes": 0.5,
|
||||
}
|
||||
@@ -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"] }
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"defName": "ExampleEarlyRiser",
|
||||
"weight": 1,
|
||||
"incompatible": ["ExampleNightOwl"],
|
||||
"commuteMinutes": 2,
|
||||
},
|
||||
{
|
||||
"defName": "ExampleNightOwl",
|
||||
"weight": 1,
|
||||
"incompatible": ["ExampleEarlyRiser"],
|
||||
"commuteMinutes": -2,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"example": "Example pack",
|
||||
"ExampleEarlyRiser": "Early riser",
|
||||
"ExampleNightOwl": "Night owl",
|
||||
"ExampleNames": "Example names",
|
||||
"ExampleStore": "Storeroom",
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"example": "Пример мода",
|
||||
"ExampleEarlyRiser": "Ранняя пташка",
|
||||
"ExampleNightOwl": "Сова",
|
||||
"ExampleNames": "Примерные имена",
|
||||
"ExampleStore": "Кладовая",
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"requires": ["core"],
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"target": "PrincipalsOffice",
|
||||
"ops": [
|
||||
{ "op": "add", "path": "/slots/-", "value": { "key": "exampleLocker", "thing": "Locker" } },
|
||||
],
|
||||
}
|
||||
@@ -28,6 +28,10 @@
|
||||
<Link>vanilla\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\src\HSchool.Server\mods\example\**\*">
|
||||
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The shipped <c>mods/example</c> 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.
|
||||
/// </summary>
|
||||
[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<CatalogResponse>(
|
||||
"/api/catalog?lang=ru&mods=example",
|
||||
TestContext.Current.CancellationToken);
|
||||
var en = await client.GetFromJsonAsync<CatalogResponse>(
|
||||
"/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<CatalogResponse>("/api/catalog?lang=ru", TestContext.Current.CancellationToken);
|
||||
var withPack = await client.GetFromJsonAsync<CatalogResponse>(
|
||||
"/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<SchoolApiTests.SchoolResponse>(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<SchoolApiTests.SchoolResponse> 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<SchoolApiTests.SchoolResponse>(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<object>() },
|
||||
},
|
||||
links = new[] { new { a = "yard", b = "store" } },
|
||||
};
|
||||
|
||||
private sealed record CatalogResponse(
|
||||
IReadOnlyList<DefInfoResponse> Territories,
|
||||
IReadOnlyList<DefInfoResponse> Buildings,
|
||||
IReadOnlyList<DefInfoResponse> Floors,
|
||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||
IReadOnlyList<DefInfoResponse> Things,
|
||||
IReadOnlyList<NameSetInfoResponse> NameSets,
|
||||
DayFrameResponse? DayFrame,
|
||||
IReadOnlyList<HolidayInfoResponse> 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<RoomSlotResponse> 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);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
namespace HSchool.Content.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The shipped <c>mods/example</c> 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.
|
||||
/// </summary>
|
||||
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<ContentDocument> 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");
|
||||
}
|
||||
@@ -25,6 +25,10 @@
|
||||
<Link>vanilla\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\src\HSchool.Server\mods\example\**\*">
|
||||
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<Link>vanilla\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\src\HSchool.Server\mods\example\**\*">
|
||||
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="golden\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<Link>vanilla\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\src\HSchool.Server\mods\example\**\*">
|
||||
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<Link>vanilla\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\src\HSchool.Server\mods\example\**\*">
|
||||
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user