Some checks failed
.NET Core / build (push) Has been cancelled
The switches used to be held by simply standing near them, and the crown captured by standing near it - clicking a switch only produced the client's "not implemented yet" message. This drives both from the original interaction instead: - Clicking a Crown Switch starts an operation which completes after CastleSiegeSettings.SwitchPushSeconds (15) and keeps the switch for the guild until its operator leaves the switch's area. One player per switch; anybody else clicking it is told another team is on it (C1 B2 14 state 2). - While one guild holds both switches the crown's shield drops for it, and its guild master captures the throne by CLICKING the crown and holding it for CrownHoldTimeSeconds - seeded to 60 now, to match the countdown the client's registration panel hardcodes. The throne stays contestable until the siege ends. - The shield now depends on the switches alone, as in the original; the gates and statues remain what they always were, the obstacle in the way. The switch info packet (C1 B2 20) is broadcast before any switch-state packet because the client's "switch released" handler reads its switch table without checking that it exists - that table is only allocated when the info packet arrives, so the wrong order crashes the client. Also fixed while in here: - The crown registration panel could never be closed: the cancel was only sent while the master still stood on the crown, which is precisely when the hold does NOT break. The panel is now closed for the player it was opened for. - A contested switch was decided by enumeration order. - Panels opened by the siege are closed when it ends. - TryCaptureThrone was dead code carrying a second, diverged rule set. - /csphase advertised the pre-refactor phase names to the client. - The periodic broadcasts keyed off "UtcNow.Second % n", which silently skips when a tick runs late; they count ticks now. The unit tests never compiled against the refactored model - they are migrated to the state machine and guild ids, and cover the new switch and crown rules. A new test proves update 105 writes the configuration into an existing database. ApplyPendingUpdatesTool applies pending configuration updates without the admin panel; it is [Explicit], so it never runs in a normal test pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
229 lines
12 KiB
C#
229 lines
12 KiB
C#
// <copyright file="TestInitializationWithEfCore.cs" company="MUnique">
|
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
// </copyright>
|
|
|
|
namespace MUnique.OpenMU.Persistence.Initialization.Tests;
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using MUnique.OpenMU.DataModel;
|
|
using MUnique.OpenMU.DataModel.Configuration;
|
|
using MUnique.OpenMU.DataModel.Entities;
|
|
using MUnique.OpenMU.GameLogic;
|
|
using MUnique.OpenMU.Persistence.EntityFramework;
|
|
using MUnique.OpenMU.Persistence.Initialization.Updates;
|
|
using MUnique.OpenMU.Persistence.InMemory;
|
|
|
|
/// <summary>
|
|
/// The main program class.
|
|
/// </summary>
|
|
[TestFixture]
|
|
internal class TestInitializationWithEfCore
|
|
{
|
|
private const byte IcarusMapNumber = 10;
|
|
private static readonly Guid FeatherDropGroupId = new(0x200, IcarusMapNumber, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
private static readonly Guid CrestDropGroupId = new(0x200, IcarusMapNumber, 2, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
|
|
/// <summary>
|
|
/// Tests the data initialization using the entity framework core.
|
|
/// </summary>
|
|
[Test]
|
|
[Ignore("This is not a real test which should run automatically.")]
|
|
public async Task SetupDatabaseAndTestLoadingDataAsync()
|
|
{
|
|
var manager = new PersistenceContextProvider(new NullLoggerFactory(), null);
|
|
using var update = await manager.ReCreateDatabaseAsync().ConfigureAwait(false);
|
|
await this.TestDataInitializationAsync(new PersistenceContextProvider(new NullLoggerFactory(), null)).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the data initialization using the in-memory persistence.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task TestDataInitializationInMemoryAsync()
|
|
{
|
|
await this.TestDataInitializationAsync(new InMemoryPersistenceContextProvider()).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the data initialization using the in-memory persistence.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task TestSeason6DataAsync()
|
|
{
|
|
var contextProvider = new InMemoryPersistenceContextProvider();
|
|
var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
|
|
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
|
await this.AssertIcarusFeatherAndCrestDropGroupsAsync(contextProvider).ConfigureAwait(false);
|
|
await this.TestIfItemsFitIntoInventoriesAsync(contextProvider).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that applying the update for Crest of Monarch in Season 6 is idempotent.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task TestSeason6CrestOfMonarchUpdatePlugInAsync()
|
|
{
|
|
var contextProvider = new InMemoryPersistenceContextProvider();
|
|
var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
|
|
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
|
|
|
using var context = contextProvider.CreateNewContext();
|
|
var gameConfiguration = (await context.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
|
var map = gameConfiguration.Maps.First(m => m.Number == IcarusMapNumber && m.Discriminator == 0);
|
|
|
|
if (gameConfiguration.DropItemGroups.FirstOrDefault(group => group.GetId() == CrestDropGroupId) is { } existingCrestGroup)
|
|
{
|
|
map.DropItemGroups.Remove(existingCrestGroup);
|
|
gameConfiguration.DropItemGroups.Remove(existingCrestGroup);
|
|
}
|
|
|
|
var update = new AddCrestOfMonarchDropGroupUpdateSeason6();
|
|
await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false);
|
|
await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false);
|
|
|
|
var groups = gameConfiguration.DropItemGroups.Where(group => group.GetId() == CrestDropGroupId).ToList();
|
|
Assert.That(groups, Has.Count.EqualTo(1));
|
|
Assert.That(map.DropItemGroups.Count(group => group.GetId() == CrestDropGroupId), Is.EqualTo(1));
|
|
Assert.That(groups[0].Chance, Is.EqualTo(0.001));
|
|
Assert.That(groups[0].MinimumMonsterLevel, Is.EqualTo((byte)82));
|
|
Assert.That(groups[0].ItemLevel, Is.EqualTo((byte)1));
|
|
Assert.That(groups[0].PossibleItems, Has.Count.EqualTo(1));
|
|
Assert.That(groups[0].PossibleItems.Single().Group, Is.EqualTo((byte)13));
|
|
Assert.That(groups[0].PossibleItems.Single().Number, Is.EqualTo((short)14));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Castle Siege update writes its configuration into an existing Season 6 database, and
|
|
/// that applying it twice does not duplicate anything. This is the update which existing servers run to
|
|
/// get the castle: without it there is no Castle Siege configuration for the event to read.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task TestSeason6CastleSiegeUpdatePlugInAsync()
|
|
{
|
|
var contextProvider = new InMemoryPersistenceContextProvider();
|
|
var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
|
|
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
|
|
|
using var context = contextProvider.CreateNewContext();
|
|
var gameConfiguration = (await context.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
|
gameConfiguration.CastleSiegeConfiguration = null;
|
|
|
|
var update = new AddCastleSiegeDataUpdatePlugIn();
|
|
await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false);
|
|
await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false);
|
|
|
|
var castleSiege = gameConfiguration.CastleSiegeConfiguration;
|
|
Assert.That(castleSiege, Is.Not.Null);
|
|
Assert.That(castleSiege!.Enabled, Is.True);
|
|
|
|
// The client's crown registration panel counts down from 60 seconds, so the server has to match it.
|
|
Assert.That(castleSiege.CrownHoldTimeSeconds, Is.EqualTo(60));
|
|
|
|
// Both Crown Switches, the crown and the throne have to be there, or the siege cannot be finished.
|
|
var npcNumbers = castleSiege.NpcDefinitions.Select(n => n.MonsterDefinition?.Number).ToList();
|
|
Assert.That(npcNumbers, Does.Contain((short)217), "Crown Switch 1");
|
|
Assert.That(npcNumbers, Does.Contain((short)218), "Crown Switch 2");
|
|
Assert.That(npcNumbers, Does.Contain((short)216), "Crown");
|
|
|
|
// Applying it twice must not double the NPC definitions.
|
|
Assert.That(npcNumbers.Count(n => n == 217), Is.EqualTo(1));
|
|
Assert.That(npcNumbers.Count(n => n == 218), Is.EqualTo(1));
|
|
|
|
var data = (await context.GetAsync<CastleSiegeData>().ConfigureAwait(false)).ToList();
|
|
Assert.That(data, Has.Count.EqualTo(1), "exactly one castle state row");
|
|
Assert.That(data[0].IsOccupied, Is.False, "a fresh castle has no owner");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the data initialization using the in-memory persistence.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test075DataAsync()
|
|
{
|
|
var contextProvider = new InMemoryPersistenceContextProvider();
|
|
var dataInitialization = new Version075.DataInitialization(contextProvider, new NullLoggerFactory());
|
|
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
|
await this.TestIfItemsFitIntoInventoriesAsync(contextProvider).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the data initialization using the in-memory persistence.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test095dDataAsync()
|
|
{
|
|
var contextProvider = new InMemoryPersistenceContextProvider();
|
|
var dataInitialization = new Version095d.DataInitialization(contextProvider, new NullLoggerFactory());
|
|
await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false);
|
|
await this.TestIfItemsFitIntoInventoriesAsync(contextProvider).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task TestDataInitializationAsync(IPersistenceContextProvider contextProvider)
|
|
{
|
|
var initialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory());
|
|
await initialization.CreateInitialDataAsync(3, true).ConfigureAwait(false);
|
|
|
|
// Loading game configuration
|
|
using var context = contextProvider.CreateNewConfigurationContext();
|
|
var gameConfiguraton = (await context.GetAsync<DataModel.Configuration.GameConfiguration>().ConfigureAwait(false)).FirstOrDefault();
|
|
Assert.That(gameConfiguraton, Is.Not.Null);
|
|
|
|
// Testing loading of an account
|
|
using var accountContext = contextProvider.CreateNewPlayerContext(gameConfiguraton!);
|
|
var account1 = await accountContext.GetAccountByLoginNameAsync("test1", "test1").ConfigureAwait(false);
|
|
Assert.That(account1, Is.Not.Null);
|
|
Assert.That(account1!.LoginName, Is.EqualTo("test1"));
|
|
}
|
|
|
|
private async Task TestIfItemsFitIntoInventoriesAsync(IPersistenceContextProvider contextProvider)
|
|
{
|
|
using var configContext = contextProvider.CreateNewConfigurationContext();
|
|
var config = (await configContext.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
|
|
|
using var context = contextProvider.CreateNewPlayerContext(config);
|
|
var characters = (await context.GetAccountsOrderedByLoginNameAsync(0, 100).ConfigureAwait(false)).SelectMany(a => a.Characters).ToList();
|
|
Assert.That(characters, Is.Not.Empty);
|
|
byte inventorySize = (byte)(InventoryConstants.EquippableSlotsCount + 64);
|
|
foreach (var character in characters)
|
|
{
|
|
try
|
|
{
|
|
var storage = character.Inventory!;
|
|
var inventory = new Storage(inventorySize, InventoryConstants.EquippableSlotsCount, 0, storage);
|
|
Assert.That(inventory.Items.Count(), Is.EqualTo(storage.Items.Count));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Assert.Warn($"{ex.Message} Character: {character.Name}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task AssertIcarusFeatherAndCrestDropGroupsAsync(IPersistenceContextProvider contextProvider)
|
|
{
|
|
using var context = contextProvider.CreateNewConfigurationContext();
|
|
var gameConfiguration = (await context.GetAsync<GameConfiguration>().ConfigureAwait(false)).First();
|
|
var map = gameConfiguration.Maps.First(m => m.Number == IcarusMapNumber && m.Discriminator == 0);
|
|
|
|
var featherGroup = gameConfiguration.DropItemGroups.Single(group => group.GetId() == FeatherDropGroupId);
|
|
var crestGroup = gameConfiguration.DropItemGroups.Single(group => group.GetId() == CrestDropGroupId);
|
|
|
|
Assert.That(map.DropItemGroups.Count(group => group.GetId() == FeatherDropGroupId), Is.EqualTo(1));
|
|
Assert.That(map.DropItemGroups.Count(group => group.GetId() == CrestDropGroupId), Is.EqualTo(1));
|
|
|
|
Assert.That(featherGroup.Chance, Is.EqualTo(0.001));
|
|
Assert.That(featherGroup.MinimumMonsterLevel, Is.EqualTo((byte)82));
|
|
Assert.That(featherGroup.ItemLevel, Is.Null);
|
|
Assert.That(featherGroup.PossibleItems, Has.Count.EqualTo(1));
|
|
Assert.That(featherGroup.PossibleItems.Single().Group, Is.EqualTo((byte)13));
|
|
Assert.That(featherGroup.PossibleItems.Single().Number, Is.EqualTo((short)14));
|
|
|
|
Assert.That(crestGroup.Chance, Is.EqualTo(0.001));
|
|
Assert.That(crestGroup.MinimumMonsterLevel, Is.EqualTo((byte)82));
|
|
Assert.That(crestGroup.ItemLevel, Is.EqualTo((byte)1));
|
|
Assert.That(crestGroup.PossibleItems, Has.Count.EqualTo(1));
|
|
Assert.That(crestGroup.PossibleItems.Single().Group, Is.EqualTo((byte)13));
|
|
Assert.That(crestGroup.PossibleItems.Single().Number, Is.EqualTo((short)14));
|
|
}
|
|
}
|