From e330084c9249d6d16567bb5c589b861b0a828a52 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Fri, 24 Apr 2026 20:41:15 +1200 Subject: [PATCH 01/14] feat: IP-bind NosMall endpoint to active game session Adds a second verification gate on top of the existing sas=MD5(sid+pid+user_id+m_szName+salt) check: the caller's X-Forwarded-For must match the IP of the user's live game session, so a leaked sas can't be redeemed from another machine. - AuthHub / AuthCodeService: new RegisterSessionIp / UnregisterSessionIp / GetSessionIp RPCs keyed by account name. - ClientSession: capture Channel.RemoteAddress on authenticate and push it into AuthHub; takes IAuthHub as a new ctor dep. - MallController: after the MD5 gate, resolve the session IP via authHub.GetSessionIpAsync and reject on mismatch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Hubs/AuthHub/AuthHub.cs | 16 ++++++++ .../Hubs/AuthHub/AuthHubClient.cs | 39 +++++++++++++++++++ .../Hubs/AuthHub/IAuthHub.cs | 4 ++ .../Networking/ClientSession/ClientSession.cs | 13 +++++++ .../Services/AuthService/AuthCodeService.cs | 16 ++++++++ .../Services/AuthService/IAuthCodeService.cs | 4 ++ .../Controllers/NosmallController.cs | 20 ++++++++-- test/NosCore.Tests.Shared/TestHelpers.cs | 1 + 8 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHub.cs b/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHub.cs index 6b948ada0..c7cb27e29 100644 --- a/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHub.cs +++ b/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHub.cs @@ -82,5 +82,21 @@ public Task StoreAuthCodeAsync(string authCode, string accountName) return Task.CompletedTask; } + public Task RegisterSessionIpAsync(string accountName, string ipAddress) + { + _authCodeService.RegisterSessionIp(accountName, ipAddress); + return Task.CompletedTask; + } + + public Task UnregisterSessionIpAsync(string accountName) + { + _authCodeService.UnregisterSessionIp(accountName); + return Task.CompletedTask; + } + + public Task GetSessionIpAsync(string accountName) + { + return Task.FromResult(_authCodeService.GetSessionIp(accountName)); + } } } diff --git a/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHubClient.cs b/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHubClient.cs index a126a6e0f..02515471e 100644 --- a/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHubClient.cs +++ b/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHubClient.cs @@ -54,5 +54,44 @@ public async Task StoreAuthCodeAsync(string authCode, string accountName) await _hubConnection.StopAsync(); } } + + public async Task RegisterSessionIpAsync(string accountName, string ipAddress) + { + await _hubConnection.StartAsync(); + try + { + await _hubConnection.InvokeAsync(nameof(RegisterSessionIpAsync), accountName, ipAddress); + } + finally + { + await _hubConnection.StopAsync(); + } + } + + public async Task UnregisterSessionIpAsync(string accountName) + { + await _hubConnection.StartAsync(); + try + { + await _hubConnection.InvokeAsync(nameof(UnregisterSessionIpAsync), accountName); + } + finally + { + await _hubConnection.StopAsync(); + } + } + + public async Task GetSessionIpAsync(string accountName) + { + await _hubConnection.StartAsync(); + try + { + return await _hubConnection.InvokeAsync(nameof(GetSessionIpAsync), accountName); + } + finally + { + await _hubConnection.StopAsync(); + } + } } } diff --git a/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/IAuthHub.cs b/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/IAuthHub.cs index 01f82bbb5..0293f7c2e 100644 --- a/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/IAuthHub.cs +++ b/src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/IAuthHub.cs @@ -20,4 +20,8 @@ public interface IAuthHub /// first) can validate it via . /// Task StoreAuthCodeAsync(string authCode, string accountName); + + Task RegisterSessionIpAsync(string accountName, string ipAddress); + Task UnregisterSessionIpAsync(string accountName); + Task GetSessionIpAsync(string accountName); } diff --git a/src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs b/src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs index 4094739db..b41dc1b95 100644 --- a/src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs +++ b/src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs @@ -11,6 +11,7 @@ using NosCore.Data.Enumerations.I18N; using NosCore.GameObject.Ecs; using NosCore.GameObject.Infastructure; +using NosCore.GameObject.InterChannelCommunication.Hubs.AuthHub; using NosCore.GameObject.InterChannelCommunication.Hubs.PubSub; using NosCore.GameObject.Services.BroadcastService; using NosCore.GameObject.Services.PacketHandlerService; @@ -38,6 +39,7 @@ public class ClientSession( IPacketHandlingStrategy packetHandlingStrategy, IEnumerable disconnectHandlers, ISessionRegistry sessionRegistry, + IAuthHub authHub, IGameLanguageLocalizer? gameLanguageLocalizer = null) : NetworkClient(logger, networkingLogLanguage, encoder), IPacketSender { @@ -115,6 +117,12 @@ public void InitializeAccount(AccountDto accountDto) AccountName = accountDto.Name, Disconnect = DisconnectAsync }); + + var remoteAddress = Channel.RemoteAddress; + if (remoteAddress != null) + { + _ = authHub.RegisterSessionIpAsync(accountDto.Name, remoteAddress); + } } } @@ -193,6 +201,11 @@ public async Task OnDisconnectedAsync() { sessionRegistry.Unregister(Channel.Id); } + if (Account != null!) + { + try { await authHub.UnregisterSessionIpAsync(Account.Name); } + catch (Exception ex) { _logger.LogWarning(ex, "Failed to unregister session IP for {Account}", Account.Name); } + } await pubSubHub.UnsubscribeAsync(SessionId); _logger.LogInformation(logLanguage[LogLanguageKey.CLIENT_DISCONNECTED]); } diff --git a/src/NosCore.GameObject/Services/AuthService/AuthCodeService.cs b/src/NosCore.GameObject/Services/AuthService/AuthCodeService.cs index de38f38d0..2db51afc3 100644 --- a/src/NosCore.GameObject/Services/AuthService/AuthCodeService.cs +++ b/src/NosCore.GameObject/Services/AuthService/AuthCodeService.cs @@ -12,6 +12,7 @@ public class AuthCodeService : IAuthCodeService { private readonly ConcurrentDictionary _authCodes = new(); private readonly ConcurrentDictionary _readyForAuth = new(); + private readonly ConcurrentDictionary _sessionIps = new(); public void StoreAuthCode(string authCode, string accountName) { @@ -42,5 +43,20 @@ public void ClearReadyForAuth(string accountName) { _readyForAuth.TryRemove(accountName, out _); } + + public void RegisterSessionIp(string accountName, string ipAddress) + { + _sessionIps[accountName] = ipAddress; + } + + public void UnregisterSessionIp(string accountName) + { + _sessionIps.TryRemove(accountName, out _); + } + + public string? GetSessionIp(string accountName) + { + return _sessionIps.TryGetValue(accountName, out var ip) ? ip : null; + } } } diff --git a/src/NosCore.GameObject/Services/AuthService/IAuthCodeService.cs b/src/NosCore.GameObject/Services/AuthService/IAuthCodeService.cs index 131dec9c1..bfa2f4303 100644 --- a/src/NosCore.GameObject/Services/AuthService/IAuthCodeService.cs +++ b/src/NosCore.GameObject/Services/AuthService/IAuthCodeService.cs @@ -15,5 +15,9 @@ public interface IAuthCodeService void MarkReadyForAuth(string accountName, long sessionId); bool IsReadyForAuth(string accountName, long sessionId); void ClearReadyForAuth(string accountName); + + void RegisterSessionIp(string accountName, string ipAddress); + void UnregisterSessionIp(string accountName); + string? GetSessionIp(string accountName); } } diff --git a/src/NosCore.WebApi/Controllers/NosmallController.cs b/src/NosCore.WebApi/Controllers/NosmallController.cs index 27f4751ce..c2c46cf56 100644 --- a/src/NosCore.WebApi/Controllers/NosmallController.cs +++ b/src/NosCore.WebApi/Controllers/NosmallController.cs @@ -1,19 +1,22 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NosCore.GameObject.InterChannelCommunication.Hubs.AuthHub; using System; +using System.Net; using System.Security.Cryptography; using System.Text; +using System.Threading.Tasks; namespace NosCore.WebApi.Controllers { [Route("[controller]")] [AllowAnonymous] // this file is an entrypoint for Mall creation. it's not recommended to add code here. - public class MallController : Controller + public class MallController(IAuthHub authHub) : Controller { [HttpGet] [Route("")] - public ActionResult Index(string sid, string pid, string user_id, string m_szName, string sas, string c, string shopType, + public async Task IndexAsync(string sid, string pid, string user_id, string m_szName, string sas, string c, string shopType, string display_language) { using var md5 = MD5.Create(); @@ -23,6 +26,17 @@ public ActionResult Index(string sid, string pid, string user_id, string m_szNam { throw new ArgumentException(null, nameof(sas)); } + + var forwarded = Request.Headers["X-Forwarded-For"].ToString(); + var parts = forwarded.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var expected = await authHub.GetSessionIpAsync(user_id); + if (parts.Length == 0 || !IPAddress.TryParse(parts[0], out var headerIp) + || string.IsNullOrEmpty(expected) || !IPAddress.TryParse(expected, out var expectedIp) + || !headerIp.Equals(expectedIp)) + { + throw new ArgumentException(null, "X-Forwarded-For"); + } + var result = Content(@$" diff --git a/test/NosCore.Tests.Shared/TestHelpers.cs b/test/NosCore.Tests.Shared/TestHelpers.cs index d562733d1..8d02ec0f9 100644 --- a/test/NosCore.Tests.Shared/TestHelpers.cs +++ b/test/NosCore.Tests.Shared/TestHelpers.cs @@ -323,6 +323,7 @@ public async Task GenerateSessionAsync(List? pack new WorldPacketHandlingStrategy(NullLogger.Instance, Instance.LogLanguageLocalizer, sessionRefHolder), new List(), Instance.SessionRegistry, + new Mock().Object, Instance.GameLanguageLocalizer) { SessionId = LastId From 21345d4142c37e27b216f471ad62aff2e056851c Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Fri, 24 Apr 2026 21:49:13 +1200 Subject: [PATCH 02/14] perf: replace Task.Delay with timestamp-driven sweeps in hot paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three MMORPG-hot paths were holding one sleeping Task per event for seconds (or longer), which multiplies per-player and per-monster. All three can be driven from the map's existing 400ms life tick instead. - WorldPacketHandlingStrategy: drop the `Task.WhenAll(handler, Task.Delay(200))` wrapper. The per-session packet lock already serializes a single client's packets; the 200ms floor was a NosCore-only artifact (vanosilla/OpenNos have nothing like it) that only served to hold a timer task per packet. - MonsterRespawnHandler: swap the fire-and-forget `Task.Delay(RespawnTime)` for `MapInstance.ScheduleRespawn(monster, respawnAt)`. The map life loop sweeps the pending-respawn table on each 400ms tick and revives whichever monsters have hit their ReadyAt. - BattleService.ScheduleCooldownReset: same idea for skill-cooldown SkillResetPacket emission. BattleService now owns a `(CharacterVisualId -> CastId -> ReadyAt)` registry. MapInstance calls `IBattleService.TickCooldownResetsAsync(this)` on each tick to drain ready entries and send the packet through the character's normal outbound path. No behavior change from the player's perspective — cooldown and respawn timers still fire with ≤400ms of latency, which is under the game's client-tick granularity. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Handlers/Battle/MonsterRespawnHandler.cs | 35 +++------------ .../WorldPacketHandlingStrategy.cs | 4 +- .../Services/BattleService/BattleService.cs | 44 ++++++++++++++----- .../Services/BattleService/IBattleService.cs | 6 +++ .../MapInstance.cs | 44 ++++++++++++++++++- .../MapInstanceGenerationService.cs | 5 ++- .../Battle/MonsterRespawnHandlerTests.cs | 2 +- .../BattleService/BattleServiceTests.cs | 1 + test/NosCore.Tests.Shared/TestHelpers.cs | 3 +- 9 files changed, 97 insertions(+), 47 deletions(-) diff --git a/src/NosCore.GameObject/Messaging/Handlers/Battle/MonsterRespawnHandler.cs b/src/NosCore.GameObject/Messaging/Handlers/Battle/MonsterRespawnHandler.cs index 155521017..5c6e66a82 100644 --- a/src/NosCore.GameObject/Messaging/Handlers/Battle/MonsterRespawnHandler.cs +++ b/src/NosCore.GameObject/Messaging/Handlers/Battle/MonsterRespawnHandler.cs @@ -7,22 +7,20 @@ using System; using System.Threading.Tasks; using JetBrains.Annotations; +using NodaTime; using NosCore.GameObject.Ecs; -using NosCore.GameObject.Ecs.Extensions; -using NosCore.GameObject.Ecs.Interfaces; using NosCore.GameObject.Messaging.Events; using NosCore.GameObject.Services.BattleService; -using NosCore.Networking; -using Microsoft.Extensions.Logging; namespace NosCore.GameObject.Messaging.Handlers.Battle { // Monster kills are driven by the closing SuPacket (alive=0, hp%=0) — the client // plays the death animation from that alone and auto-despawns the sprite. Sending // an OutPacket here would cut the animation short, so we only clear aggro and - // schedule the respawn. Player deaths are handled by the revive/warp flow. + // register the respawn timestamp on the map; the map's 400ms life loop reads the + // pending-respawn table and revives monsters whose time is up. [UsedImplicitly] - public sealed class MonsterRespawnHandler(IAggroService aggroService, ILogger logger) + public sealed class MonsterRespawnHandler(IAggroService aggroService, IClock clock) { [UsedImplicitly] public Task Handle(EntityDiedEvent evt) @@ -34,31 +32,8 @@ public Task Handle(EntityDiedEvent evt) aggroService.Clear(monster); var respawnMs = Math.Max(1000, monster.NpcMonster.RespawnTime); - _ = RespawnAfterDelayAsync(monster, respawnMs); + monster.MapInstance.ScheduleRespawn(monster, clock.GetCurrentInstant().Plus(Duration.FromMilliseconds(respawnMs))); return Task.CompletedTask; } - - private async Task RespawnAfterDelayAsync(MonsterComponentBundle monster, int delayMs) - { - try - { - await Task.Delay(delayMs).ConfigureAwait(false); - // Map may have been disposed between death and respawn — bail if so. - if (monster.MapInstance == null) return; - - monster.Hp = monster.MaxHp; - monster.Mp = monster.MaxMp; - monster.IsAlive = true; - monster.PositionX = monster.FirstX; - monster.PositionY = monster.FirstY; - monster.HitList.Clear(); - - await monster.MapInstance.SendPacketAsync(monster.GenerateIn()).ConfigureAwait(false); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to respawn monster {VisualId}", monster.VisualId); - } - } } } diff --git a/src/NosCore.GameObject/Networking/ClientSession/WorldPacketHandlingStrategy.cs b/src/NosCore.GameObject/Networking/ClientSession/WorldPacketHandlingStrategy.cs index 3d5906459..6a841b84b 100644 --- a/src/NosCore.GameObject/Networking/ClientSession/WorldPacketHandlingStrategy.cs +++ b/src/NosCore.GameObject/Networking/ClientSession/WorldPacketHandlingStrategy.cs @@ -192,12 +192,12 @@ private async Task ExecuteHandlerAsync(IPacketHandler handler, IPacket packet, C { using (await session.AcquirePacketLockAsync().ConfigureAwait(false)) { - await Task.WhenAll(handler.ExecuteAsync(packet, session), Task.Delay(200)).ConfigureAwait(false); + await handler.ExecuteAsync(packet, session).ConfigureAwait(false); } } else { - await Task.WhenAll(handler.ExecuteAsync(packet, session), Task.Delay(200)).ConfigureAwait(false); + await handler.ExecuteAsync(packet, session).ConfigureAwait(false); } } } diff --git a/src/NosCore.GameObject/Services/BattleService/BattleService.cs b/src/NosCore.GameObject/Services/BattleService/BattleService.cs index 8c584c3d4..d71808ba2 100644 --- a/src/NosCore.GameObject/Services/BattleService/BattleService.cs +++ b/src/NosCore.GameObject/Services/BattleService/BattleService.cs @@ -5,15 +5,18 @@ // using System; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using System.Linq; +using NodaTime; using NosCore.GameObject.Ecs; using NosCore.GameObject.Ecs.Extensions; using NosCore.GameObject.Ecs.Interfaces; using NosCore.GameObject.Messaging.Events; using NosCore.GameObject.Services.BattleService.Model; using NosCore.GameObject.Services.BroadcastService; +using NosCore.GameObject.Services.MapInstanceGenerationService; using NosCore.Networking; using NosCore.Packets.Enumerations; using NosCore.Shared.Enumerations; @@ -33,8 +36,13 @@ public sealed class BattleService( IHitQueue hitQueue, IMessageBus messageBus, ISessionRegistry sessionRegistry, + IClock clock, ILogger logger) : IBattleService { + // CharacterId (VisualId) → CastId → ReadyAt. Populated by ScheduleCooldownReset + // and drained by TickCooldownResetsAsync on each map's 400ms life tick. + private readonly ConcurrentDictionary> _pendingCooldownResets = new(); + public async Task Hit(IAliveEntity origin, IAliveEntity target, HitArguments arguments) { if (!CanAttack(origin, target)) @@ -187,19 +195,35 @@ private void ScheduleCooldownReset(IAliveEntity origin, SkillInfo skill) { if (origin is not ICharacterEntity character) return; - _ = Task.Run(async () => + var readyAt = clock.GetCurrentInstant().Plus(Duration.FromMilliseconds(skill.Cooldown * 100)); + _pendingCooldownResets + .GetOrAdd(character.VisualId, _ => new ConcurrentDictionary())[skill.CastId] = readyAt; + } + + public async Task TickCooldownResetsAsync(MapInstance mapInstance) + { + if (_pendingCooldownResets.IsEmpty) return; + var now = clock.GetCurrentInstant(); + foreach (var session in sessionRegistry.GetClientSessionsByMapInstance(mapInstance.MapInstanceId)) { - try + if (!session.HasPlayerEntity) continue; + var character = session.Character; + if (!_pendingCooldownResets.TryGetValue(character.VisualId, out var skills)) continue; + foreach (var (castId, readyAt) in skills) { - await Task.Delay(skill.Cooldown * 100).ConfigureAwait(false); - if (character.IsDisconnecting) return; - await character.SendPacketAsync(new SkillResetPacket { CastId = skill.CastId }).ConfigureAwait(false); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to reset cooldown for skill {CastId}", skill.CastId); + if (readyAt > now) continue; + if (!skills.TryRemove(castId, out _)) continue; + if (character.IsDisconnecting) continue; + try + { + await character.SendPacketAsync(new SkillResetPacket { CastId = castId }).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to reset cooldown for skill {CastId}", castId); + } } - }); + } } private static void UpdateAttackerPosition(IAliveEntity origin, HitArguments arguments) diff --git a/src/NosCore.GameObject/Services/BattleService/IBattleService.cs b/src/NosCore.GameObject/Services/BattleService/IBattleService.cs index 23fb2e95d..58e417ff0 100644 --- a/src/NosCore.GameObject/Services/BattleService/IBattleService.cs +++ b/src/NosCore.GameObject/Services/BattleService/IBattleService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using NosCore.GameObject.Ecs.Interfaces; +using NosCore.GameObject.Services.MapInstanceGenerationService; namespace NosCore.GameObject.Services.BattleService { @@ -16,5 +17,10 @@ namespace NosCore.GameObject.Services.BattleService public interface IBattleService { Task Hit(IAliveEntity origin, IAliveEntity target, HitArguments arguments); + + // Drain the pending skill-cooldown-reset table for characters on this map + // and emit SkillResetPacket for any whose ReadyAt has elapsed. Called once + // per map per 400ms life tick from MapInstance. + Task TickCooldownResetsAsync(MapInstance mapInstance); } } diff --git a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs index 5248021cd..4ca2e9525 100644 --- a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs +++ b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs @@ -21,6 +21,7 @@ using NosCore.GameObject.Services.MapChangeService; using NosCore.GameObject.Services.MapItemGenerationService; using NosCore.GameObject.Services.MinilandService; +using NosCore.Networking; using NosCore.Networking.SessionGroup; using NosCore.Packets.Interfaces; using NosCore.PathFinder.Interfaces; @@ -62,13 +63,16 @@ public class MapInstance : IBroadcastable, IDisposable private readonly IMonsterAi? _monsterAi; private readonly IBuffService? _buffService; private readonly IRegenerationService? _regenerationService; + private readonly IBattleService? _battleService; + private readonly ConcurrentDictionary _pendingRespawns = new(); public MapWorld EcsWorld { get; } public MapInstance(Map.Map map, Guid guid, bool shopAllowed, MapInstanceType type, IMapItemGenerationService mapItemGenerationService, ILogger logger, IClock clock, IMapChangeService mapChangeService, ISessionGroupFactory sessionGroupFactory, ISessionRegistry sessionRegistry, IHeuristic distanceCalculator, - IMonsterAi? monsterAi = null, IBuffService? buffService = null, IRegenerationService? regenerationService = null) + IMonsterAi? monsterAi = null, IBuffService? buffService = null, IRegenerationService? regenerationService = null, + IBattleService? battleService = null) { LastPackets = new ConcurrentQueue(); XpRate = 1; @@ -93,6 +97,7 @@ public MapInstance(Map.Map map, Guid guid, bool shopAllowed, MapInstanceType typ _monsterAi = monsterAi; _buffService = buffService; _regenerationService = regenerationService; + _battleService = battleService; EcsWorld = new MapWorld(); } @@ -424,6 +429,13 @@ async Task LifeAsync() { await _regenerationService.TickAsync(this).ConfigureAwait(false); } + + if (_battleService != null) + { + await _battleService.TickCooldownResetsAsync(this).ConfigureAwait(false); + } + + await SweepPendingRespawnsAsync().ConfigureAwait(false); } catch (Exception e) { @@ -434,6 +446,36 @@ async Task LifeAsync() return Task.CompletedTask; } + // Registered by MonsterRespawnHandler when a monster dies. The map-level life + // loop picks it up on the next 400ms tick instead of us spawning a Task.Delay + // per corpse. + public void ScheduleRespawn(MonsterComponentBundle monster, Instant respawnAt) + { + _pendingRespawns[monster.VisualId] = (monster, respawnAt); + } + + private async Task SweepPendingRespawnsAsync() + { + if (_pendingRespawns.IsEmpty) return; + var now = _clock.GetCurrentInstant(); + foreach (var (visualId, entry) in _pendingRespawns) + { + if (entry.RespawnAt > now) continue; + if (!_pendingRespawns.TryRemove(visualId, out _)) continue; + var monster = entry.Monster; + if (monster.MapInstance == null) continue; + + monster.Hp = monster.MaxHp; + monster.Mp = monster.MaxMp; + monster.IsAlive = true; + monster.PositionX = monster.FirstX; + monster.PositionY = monster.FirstY; + monster.HitList.Clear(); + + await this.SendPacketAsync(monster.GenerateIn()).ConfigureAwait(false); + } + } + protected virtual void Dispose(bool disposing) { if (!disposing) diff --git a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstanceGenerationService.cs b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstanceGenerationService.cs index 3d55b47fc..71d2aecdd 100644 --- a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstanceGenerationService.cs +++ b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstanceGenerationService.cs @@ -40,7 +40,8 @@ public class MapInstanceGeneratorService(List maps, List ISessionGroupFactory sessionGroupFactory, ISessionRegistry sessionRegistry, IItemGenerationService itemProvider, IHeuristic distanceCalculator, NosCore.GameObject.Services.BattleService.IMonsterAi monsterAi, NosCore.GameObject.Services.BattleService.IBuffService buffService, - NosCore.GameObject.Services.BattleService.IRegenerationService regenerationService) + NosCore.GameObject.Services.BattleService.IRegenerationService regenerationService, + NosCore.GameObject.Services.BattleService.IBattleService battleService) : IMapInstanceGeneratorService { public Task AddMapInstanceAsync(MapInstance mapInstance) @@ -128,7 +129,7 @@ public MapInstance CreateMapInstance(Map.Map map, Guid guid, bool shopAllowed, M { return new MapInstance(map, guid, shopAllowed, normalInstance, mapItemGenerationService, loggerFactory.CreateLogger(), clock, - mapChangeService, sessionGroupFactory, sessionRegistry, distanceCalculator, monsterAi, buffService, regenerationService); + mapChangeService, sessionGroupFactory, sessionRegistry, distanceCalculator, monsterAi, buffService, regenerationService, battleService); } private async Task LoadPortalsAsync(MapInstance mapInstance, List portals) diff --git a/test/NosCore.GameObject.Tests/Messaging/Handlers/Battle/MonsterRespawnHandlerTests.cs b/test/NosCore.GameObject.Tests/Messaging/Handlers/Battle/MonsterRespawnHandlerTests.cs index a3778c83c..168b350b6 100644 --- a/test/NosCore.GameObject.Tests/Messaging/Handlers/Battle/MonsterRespawnHandlerTests.cs +++ b/test/NosCore.GameObject.Tests/Messaging/Handlers/Battle/MonsterRespawnHandlerTests.cs @@ -25,7 +25,7 @@ public async Task PlayerDeathIsIgnored() // Passing a non-monster victim should be a no-op (no aggro clear either, // since the player doesn't own monster aggro state). var aggro = new Mock(); - var handler = new MonsterRespawnHandler(aggro.Object, new Mock>().Object); + var handler = new MonsterRespawnHandler(aggro.Object, NodaTime.SystemClock.Instance); var playerVictim = new Mock().Object; var killer = new Mock().Object; diff --git a/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs b/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs index 18de5e0ab..e98cc4508 100644 --- a/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs +++ b/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs @@ -49,6 +49,7 @@ public void Setup() _hitQueue.Object, _bus.Object, new Mock().Object, + NodaTime.SystemClock.Instance, NullLogger.Instance); } diff --git a/test/NosCore.Tests.Shared/TestHelpers.cs b/test/NosCore.Tests.Shared/TestHelpers.cs index 8d02ec0f9..a014d3c39 100644 --- a/test/NosCore.Tests.Shared/TestHelpers.cs +++ b/test/NosCore.Tests.Shared/TestHelpers.cs @@ -255,7 +255,8 @@ private async Task GenerateMapInstanceProviderAsync() mapInstanceRegistry, MapInstanceAccessorService, Instance.Clock, Instance.LogLanguageLocalizer, mapChangeService, SessionGroupFactory, SessionRegistry, GenerateItemProvider(), Instance.DistanceCalculator, new Mock().Object, new Mock().Object, - new Mock().Object); + new Mock().Object, + new Mock().Object); await instanceGeneratorService.InitializeAsync(); await instanceGeneratorService.AddMapInstanceAsync(new MapInstance(miniland, MinilandId, false, MapInstanceType.NormalInstance, MapItemProvider, NullLogger.Instance, Clock, mapChangeService, SessionGroupFactory, SessionRegistry, Instance.DistanceCalculator)); From 7f0f65675547b64421a1663073c07c3eb9718015 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 00:23:51 +1200 Subject: [PATCH 03/14] feat: add $ChangeGender / $SetHairStyle / $SetHairColor GM commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three missing cosmetic GM commands that parallel the existing item-use handlers (ChangeGenderHandler, HairDieHandler) — same refresh sequence (GenerateEq + GenerateIn + GenerateCMode/GenerateEff for gender), same enum validation (Enum.IsDefined guard), no bypass of sanity checks beyond what a GM command is entitled to. - $ChangeGender: flips the character's gender and plays the standard visual-refresh + effect 196 swap that the in-game gender card uses. - $SetHairStyle Style: sets HairStyleType; ignores out-of-range values. - $SetHairColor Color: sets HairColorType; same guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CommandPackets/ChangeGenderPacket.cs | 19 ++++++++++ .../CommandPackets/SetHairColorPacket.cs | 23 ++++++++++++ .../CommandPackets/SetHairStylePacket.cs | 23 ++++++++++++ .../Command/ChangeGenderPacketHandler.cs | 31 ++++++++++++++++ .../Command/SetHairColorPacketHandler.cs | 35 +++++++++++++++++++ .../Command/SetHairStylePacketHandler.cs | 35 +++++++++++++++++++ 6 files changed, 166 insertions(+) create mode 100644 src/NosCore.Data/CommandPackets/ChangeGenderPacket.cs create mode 100644 src/NosCore.Data/CommandPackets/SetHairColorPacket.cs create mode 100644 src/NosCore.Data/CommandPackets/SetHairStylePacket.cs create mode 100644 src/NosCore.PacketHandlers/Command/ChangeGenderPacketHandler.cs create mode 100644 src/NosCore.PacketHandlers/Command/SetHairColorPacketHandler.cs create mode 100644 src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs diff --git a/src/NosCore.Data/CommandPackets/ChangeGenderPacket.cs b/src/NosCore.Data/CommandPackets/ChangeGenderPacket.cs new file mode 100644 index 000000000..55b828f21 --- /dev/null +++ b/src/NosCore.Data/CommandPackets/ChangeGenderPacket.cs @@ -0,0 +1,19 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$ChangeGender", AuthorityType.GameMaster)] + public class ChangeGenderPacket : CommandPacket + { + public override string Help() + { + return "$ChangeGender"; + } + } +} diff --git a/src/NosCore.Data/CommandPackets/SetHairColorPacket.cs b/src/NosCore.Data/CommandPackets/SetHairColorPacket.cs new file mode 100644 index 000000000..cc151fb78 --- /dev/null +++ b/src/NosCore.Data/CommandPackets/SetHairColorPacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$SetHairColor", AuthorityType.GameMaster)] + public class SetHairColorPacket : CommandPacket + { + [PacketIndex(0)] + public byte Color { get; set; } + + public override string Help() + { + return "$SetHairColor Color"; + } + } +} diff --git a/src/NosCore.Data/CommandPackets/SetHairStylePacket.cs b/src/NosCore.Data/CommandPackets/SetHairStylePacket.cs new file mode 100644 index 000000000..747f0ad0f --- /dev/null +++ b/src/NosCore.Data/CommandPackets/SetHairStylePacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$SetHairStyle", AuthorityType.GameMaster)] + public class SetHairStylePacket : CommandPacket + { + [PacketIndex(0)] + public byte Style { get; set; } + + public override string Help() + { + return "$SetHairStyle Style"; + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/ChangeGenderPacketHandler.cs b/src/NosCore.PacketHandlers/Command/ChangeGenderPacketHandler.cs new file mode 100644 index 000000000..be6b8ee8d --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/ChangeGenderPacketHandler.cs @@ -0,0 +1,31 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; +using NosCore.Packets.Enumerations; + +namespace NosCore.PacketHandlers.Command +{ + public class ChangeGenderPacketHandler : PacketHandler, IWorldPacketHandler + { + public override async Task ExecuteAsync(ChangeGenderPacket _, ClientSession session) + { + var character = session.Character; + character.Gender = character.Gender == GenderType.Female ? GenderType.Male : GenderType.Female; + + await session.SendPacketAsync(character.GenerateEq()); + await character.MapInstance.SendPacketAsync(character.GenerateIn(string.Empty)); + await character.MapInstance.SendPacketAsync(character.GenerateCMode()); + await character.MapInstance.SendPacketAsync(character.GenerateEff(196)); + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/SetHairColorPacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetHairColorPacketHandler.cs new file mode 100644 index 000000000..72dc7d3cd --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/SetHairColorPacketHandler.cs @@ -0,0 +1,35 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System; +using System.Threading.Tasks; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; +using NosCore.Packets.Enumerations; + +namespace NosCore.PacketHandlers.Command +{ + public class SetHairColorPacketHandler : PacketHandler, IWorldPacketHandler + { + public override async Task ExecuteAsync(SetHairColorPacket packet, ClientSession session) + { + if (!Enum.IsDefined(typeof(HairColorType), packet.Color)) + { + return; + } + + var character = session.Character; + character.HairColor = (HairColorType)packet.Color; + + await session.SendPacketAsync(character.GenerateEq()); + await character.MapInstance.SendPacketAsync(character.GenerateIn(string.Empty)); + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs new file mode 100644 index 000000000..7d9a11436 --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs @@ -0,0 +1,35 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System; +using System.Threading.Tasks; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; +using NosCore.Packets.Enumerations; + +namespace NosCore.PacketHandlers.Command +{ + public class SetHairStylePacketHandler : PacketHandler, IWorldPacketHandler + { + public override async Task ExecuteAsync(SetHairStylePacket packet, ClientSession session) + { + if (!Enum.IsDefined(typeof(HairStyleType), packet.Style)) + { + return; + } + + var character = session.Character; + character.HairStyle = (HairStyleType)packet.Style; + + await session.SendPacketAsync(character.GenerateEq()); + await character.MapInstance.SendPacketAsync(character.GenerateIn(string.Empty)); + } + } +} From 7e5ef418ef5887294eb449f44d75257c9ecf427b Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 00:39:19 +1200 Subject: [PATCH 04/14] feat: add $ShoutHere / $Kill / $SetSpPoint GM commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more GM commands that reuse existing building blocks: - $ShoutHere Message — Moderator — broadcasts a yellow chat line to the caller's current map only. Complements the server-wide $Shout. - $Kill — GameMaster — sets the caller's HP to 0 and publishes an EntityDiedEvent so PlayerRevivalHandler runs its normal death + respawn flow. Self-target only for now; cross-target kill would need the cross-channel StatData pattern like $SetLevel / $SetGold. - $SetSpPoint Value — GameMaster — clamps to WorldConfiguration bounds, writes character.SpPoint, and refreshes the client via GenerateSpPoint. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/NosCore.Data/CommandPackets/KillPacket.cs | 19 ++++++++++++ .../CommandPackets/SetSpPointPacket.cs | 23 ++++++++++++++ .../CommandPackets/ShoutHerePacket.cs | 23 ++++++++++++++ .../Command/KillCommandPacketHandler.cs | 24 +++++++++++++++ .../Command/SetSpPointPacketHandler.cs | 30 +++++++++++++++++++ .../Command/ShoutHerePacketHandler.cs | 30 +++++++++++++++++++ 6 files changed, 149 insertions(+) create mode 100644 src/NosCore.Data/CommandPackets/KillPacket.cs create mode 100644 src/NosCore.Data/CommandPackets/SetSpPointPacket.cs create mode 100644 src/NosCore.Data/CommandPackets/ShoutHerePacket.cs create mode 100644 src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs create mode 100644 src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs create mode 100644 src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs diff --git a/src/NosCore.Data/CommandPackets/KillPacket.cs b/src/NosCore.Data/CommandPackets/KillPacket.cs new file mode 100644 index 000000000..0dd37b8ba --- /dev/null +++ b/src/NosCore.Data/CommandPackets/KillPacket.cs @@ -0,0 +1,19 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$Kill", AuthorityType.GameMaster)] + public class KillPacket : CommandPacket + { + public override string Help() + { + return "$Kill"; + } + } +} diff --git a/src/NosCore.Data/CommandPackets/SetSpPointPacket.cs b/src/NosCore.Data/CommandPackets/SetSpPointPacket.cs new file mode 100644 index 000000000..26ab6eba3 --- /dev/null +++ b/src/NosCore.Data/CommandPackets/SetSpPointPacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$SetSpPoint", AuthorityType.GameMaster)] + public class SetSpPointPacket : CommandPacket + { + [PacketIndex(0)] + public int SpPoint { get; set; } + + public override string Help() + { + return "$SetSpPoint Value"; + } + } +} diff --git a/src/NosCore.Data/CommandPackets/ShoutHerePacket.cs b/src/NosCore.Data/CommandPackets/ShoutHerePacket.cs new file mode 100644 index 000000000..852d3d688 --- /dev/null +++ b/src/NosCore.Data/CommandPackets/ShoutHerePacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$ShoutHere", AuthorityType.Moderator)] + public class ShoutHerePacket : CommandPacket + { + [PacketIndex(0)] + public string? Message { get; set; } + + public override string Help() + { + return "$ShoutHere Message"; + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs b/src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs new file mode 100644 index 000000000..346be3620 --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs @@ -0,0 +1,24 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Messaging.Events; +using NosCore.GameObject.Networking.ClientSession; +using Wolverine; + +namespace NosCore.PacketHandlers.Command +{ + public class KillCommandPacketHandler(IMessageBus messageBus) : PacketHandler, IWorldPacketHandler + { + public override async Task ExecuteAsync(KillPacket _, ClientSession session) + { + session.Character.Hp = 0; + await messageBus.PublishAsync(new EntityDiedEvent(session.Character, session.Character)).ConfigureAwait(false); + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs new file mode 100644 index 000000000..7222b7f40 --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs @@ -0,0 +1,30 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NosCore.Core.Configuration; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; + +namespace NosCore.PacketHandlers.Command +{ + public class SetSpPointPacketHandler(IOptions worldConfiguration) + : PacketHandler, IWorldPacketHandler + { + public override Task ExecuteAsync(SetSpPointPacket packet, ClientSession session) + { + var clamped = packet.SpPoint < 0 ? 0 + : packet.SpPoint > worldConfiguration.Value.MaxSpPoints ? worldConfiguration.Value.MaxSpPoints + : packet.SpPoint; + session.Character.SpPoint = clamped; + return session.SendPacketAsync(session.Character.GenerateSpPoint(worldConfiguration)); + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs b/src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs new file mode 100644 index 000000000..7663d8521 --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs @@ -0,0 +1,30 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; +using NosCore.Packets.Enumerations; + +namespace NosCore.PacketHandlers.Command +{ + public class ShoutHerePacketHandler : PacketHandler, IWorldPacketHandler + { + public override Task ExecuteAsync(ShoutHerePacket packet, ClientSession session) + { + if (string.IsNullOrEmpty(packet.Message)) + { + return session.SendPacketAsync(session.Character.GenerateSay(packet.Help(), SayColorType.Yellow)); + } + + return session.Character.MapInstance.SendPacketAsync( + session.Character.GenerateSay(packet.Message, SayColorType.Yellow)); + } + } +} From 843e529e6f28679f01e0b792664525f4ab1dbbf0 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 01:11:50 +1200 Subject: [PATCH 05/14] feat: add $SetBankGold / $SetSpAdditionPoint / $SetJobLevelXp / $SetHeroXp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four more single-field GM commands that only wire existing ICharacterEntity writable properties to a command packet. No new infrastructure. - $SetBankGold Amount — writes Character.BankGold. Bank UI refreshes on next open; no separate refresh packet needed. - $SetSpAdditionPoint Value — clamps to WorldConfiguration. MaxAdditionalSpPoints and refreshes via GenerateSpPoint, same shape as the just-added $SetSpPoint. - $SetJobLevelXp Amount — writes Character.JobLevelXp and emits GenerateLev so the XP bars sync immediately. - $SetHeroXp Amount — writes Character.HeroXp; same GenerateLev refresh. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CommandPackets/SetBankGoldPacket.cs | 23 ++++++++++++++ .../CommandPackets/SetHeroXpPacket.cs | 23 ++++++++++++++ .../CommandPackets/SetJobLevelXpPacket.cs | 23 ++++++++++++++ .../SetSpAdditionPointPacket.cs | 23 ++++++++++++++ .../Command/SetBankGoldPacketHandler.cs | 22 ++++++++++++++ .../Command/SetHeroXpPacketHandler.cs | 29 ++++++++++++++++++ .../Command/SetJobLevelXpPacketHandler.cs | 29 ++++++++++++++++++ .../SetSpAdditionPointPacketHandler.cs | 30 +++++++++++++++++++ 8 files changed, 202 insertions(+) create mode 100644 src/NosCore.Data/CommandPackets/SetBankGoldPacket.cs create mode 100644 src/NosCore.Data/CommandPackets/SetHeroXpPacket.cs create mode 100644 src/NosCore.Data/CommandPackets/SetJobLevelXpPacket.cs create mode 100644 src/NosCore.Data/CommandPackets/SetSpAdditionPointPacket.cs create mode 100644 src/NosCore.PacketHandlers/Command/SetBankGoldPacketHandler.cs create mode 100644 src/NosCore.PacketHandlers/Command/SetHeroXpPacketHandler.cs create mode 100644 src/NosCore.PacketHandlers/Command/SetJobLevelXpPacketHandler.cs create mode 100644 src/NosCore.PacketHandlers/Command/SetSpAdditionPointPacketHandler.cs diff --git a/src/NosCore.Data/CommandPackets/SetBankGoldPacket.cs b/src/NosCore.Data/CommandPackets/SetBankGoldPacket.cs new file mode 100644 index 000000000..036dcbd7b --- /dev/null +++ b/src/NosCore.Data/CommandPackets/SetBankGoldPacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$SetBankGold", AuthorityType.GameMaster)] + public class SetBankGoldPacket : CommandPacket + { + [PacketIndex(0)] + public long BankGold { get; set; } + + public override string Help() + { + return "$SetBankGold Amount"; + } + } +} diff --git a/src/NosCore.Data/CommandPackets/SetHeroXpPacket.cs b/src/NosCore.Data/CommandPackets/SetHeroXpPacket.cs new file mode 100644 index 000000000..22149ca70 --- /dev/null +++ b/src/NosCore.Data/CommandPackets/SetHeroXpPacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$SetHeroXp", AuthorityType.GameMaster)] + public class SetHeroXpPacket : CommandPacket + { + [PacketIndex(0)] + public long HeroXp { get; set; } + + public override string Help() + { + return "$SetHeroXp Amount"; + } + } +} diff --git a/src/NosCore.Data/CommandPackets/SetJobLevelXpPacket.cs b/src/NosCore.Data/CommandPackets/SetJobLevelXpPacket.cs new file mode 100644 index 000000000..06f02ca11 --- /dev/null +++ b/src/NosCore.Data/CommandPackets/SetJobLevelXpPacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$SetJobLevelXp", AuthorityType.GameMaster)] + public class SetJobLevelXpPacket : CommandPacket + { + [PacketIndex(0)] + public long JobLevelXp { get; set; } + + public override string Help() + { + return "$SetJobLevelXp Amount"; + } + } +} diff --git a/src/NosCore.Data/CommandPackets/SetSpAdditionPointPacket.cs b/src/NosCore.Data/CommandPackets/SetSpAdditionPointPacket.cs new file mode 100644 index 000000000..927ecf7be --- /dev/null +++ b/src/NosCore.Data/CommandPackets/SetSpAdditionPointPacket.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.Packets.Attributes; +using NosCore.Shared.Enumerations; + +namespace NosCore.Data.CommandPackets +{ + [CommandPacketHeader("$SetSpAdditionPoint", AuthorityType.GameMaster)] + public class SetSpAdditionPointPacket : CommandPacket + { + [PacketIndex(0)] + public int SpAdditionPoint { get; set; } + + public override string Help() + { + return "$SetSpAdditionPoint Value"; + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/SetBankGoldPacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetBankGoldPacketHandler.cs new file mode 100644 index 000000000..be42798c2 --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/SetBankGoldPacketHandler.cs @@ -0,0 +1,22 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking.ClientSession; + +namespace NosCore.PacketHandlers.Command +{ + public class SetBankGoldPacketHandler : PacketHandler, IWorldPacketHandler + { + public override Task ExecuteAsync(SetBankGoldPacket packet, ClientSession session) + { + session.Character.BankGold = packet.BankGold < 0 ? 0 : packet.BankGold; + return Task.CompletedTask; + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/SetHeroXpPacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetHeroXpPacketHandler.cs new file mode 100644 index 000000000..c9557e70a --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/SetHeroXpPacketHandler.cs @@ -0,0 +1,29 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using NosCore.Algorithm.ExperienceService; +using NosCore.Algorithm.HeroExperienceService; +using NosCore.Algorithm.JobExperienceService; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; + +namespace NosCore.PacketHandlers.Command +{ + public class SetHeroXpPacketHandler(IExperienceService experienceService, + IJobExperienceService jobExperienceService, IHeroExperienceService heroExperienceService) + : PacketHandler, IWorldPacketHandler + { + public override Task ExecuteAsync(SetHeroXpPacket packet, ClientSession session) + { + session.Character.HeroXp = packet.HeroXp < 0 ? 0 : packet.HeroXp; + return session.SendPacketAsync(session.Character.GenerateLev(experienceService, jobExperienceService, heroExperienceService)); + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/SetJobLevelXpPacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetJobLevelXpPacketHandler.cs new file mode 100644 index 000000000..5dc9b46ee --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/SetJobLevelXpPacketHandler.cs @@ -0,0 +1,29 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using NosCore.Algorithm.ExperienceService; +using NosCore.Algorithm.HeroExperienceService; +using NosCore.Algorithm.JobExperienceService; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; + +namespace NosCore.PacketHandlers.Command +{ + public class SetJobLevelXpPacketHandler(IExperienceService experienceService, + IJobExperienceService jobExperienceService, IHeroExperienceService heroExperienceService) + : PacketHandler, IWorldPacketHandler + { + public override Task ExecuteAsync(SetJobLevelXpPacket packet, ClientSession session) + { + session.Character.JobLevelXp = packet.JobLevelXp < 0 ? 0 : packet.JobLevelXp; + return session.SendPacketAsync(session.Character.GenerateLev(experienceService, jobExperienceService, heroExperienceService)); + } + } +} diff --git a/src/NosCore.PacketHandlers/Command/SetSpAdditionPointPacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetSpAdditionPointPacketHandler.cs new file mode 100644 index 000000000..ca25e653f --- /dev/null +++ b/src/NosCore.PacketHandlers/Command/SetSpAdditionPointPacketHandler.cs @@ -0,0 +1,30 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NosCore.Core.Configuration; +using NosCore.Data.CommandPackets; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.GameObject.Infastructure; +using NosCore.GameObject.Networking.ClientSession; +using NosCore.Networking; + +namespace NosCore.PacketHandlers.Command +{ + public class SetSpAdditionPointPacketHandler(IOptions worldConfiguration) + : PacketHandler, IWorldPacketHandler + { + public override Task ExecuteAsync(SetSpAdditionPointPacket packet, ClientSession session) + { + var clamped = packet.SpAdditionPoint < 0 ? 0 + : packet.SpAdditionPoint > worldConfiguration.Value.MaxAdditionalSpPoints ? worldConfiguration.Value.MaxAdditionalSpPoints + : packet.SpAdditionPoint; + session.Character.SpAdditionPoint = clamped; + return session.SendPacketAsync(session.Character.GenerateSpPoint(worldConfiguration)); + } + } +} From 4e514cbda3774a94807290103b567fa0a9404054 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 01:34:16 +1200 Subject: [PATCH 06/14] feat: pet capture via BCardType.Capture / CaptureAnimal skill bcard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture is a skill-side mechanic, not an item effect. Matches the vanosilla + OpenNos implementation: - Skill carries a `BCardType.Capture` bcard with subtype `AdditionalTypes.Capture.CaptureAnimal` (same enum codes as both reference servers). - BattleService.Hit detects the bcard before damage resolution and delegates to ICaptureService, skipping the normal damage pipeline (no SuPacket, no stat broadcast, no EntityDamagedEvent) — matches vanosilla BattleSystem's `if (!skill.BCards.Any(... Capture ...))` guards around the SuPacket sends. - Cooldown still fires so a failed roll consumes the gate. CaptureService rules (cross-referenced with both servers): - Caster must be a player, target must be a live monster. - monster.Level <= player.Level. - monster.HP% < 50 (matches vanosilla BCardCaptureHandler gate). - Rate: 90 % if player < level 20, else 50 % (vanosilla constants). - Mate level = max(monster.Level - 15, 1) (vanosilla offset). - On success: insert MateDto via the auto-generated IDao, mark monster !IsAlive + Hp = 0, broadcast `out` to despawn the sprite. Followups (not in this PR): dignity + MaxMateCount + RaidInstance guards, SuCapturePacket (vanosilla's dedicated capture-su), in-game mate list refresh post-entry (sc_n / p_clear / pinit). Co-Authored-By: Claude Opus 4.7 (1M context) --- Directory.Packages.props | 2 +- .../Enumerations/Items/ItemEffectType.cs | 1 + .../Extensions/CharacterEntityExtension.cs | 4 +- .../Messaging/Events/EntityCapturedEvent.cs | 17 +++ .../Handlers/Quest/OnEntityCapturedHandler.cs | 39 ++++++ .../Services/BattleService/BattleService.cs | 19 +++ .../Services/BattleService/CaptureService.cs | 127 ++++++++++++++++++ .../Services/BattleService/ICaptureService.cs | 28 ++++ .../Services/QuestService/IQuestService.cs | 6 + .../Services/QuestService/Quest.cs | 39 +++--- .../Services/QuestService/QuestService.cs | 9 ++ .../Command/SetHairStylePacketHandler.cs | 3 +- .../BattleService/BattleServiceTests.cs | 1 + 13 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 src/NosCore.GameObject/Messaging/Events/EntityCapturedEvent.cs create mode 100644 src/NosCore.GameObject/Messaging/Handlers/Quest/OnEntityCapturedHandler.cs create mode 100644 src/NosCore.GameObject/Services/BattleService/CaptureService.cs create mode 100644 src/NosCore.GameObject/Services/BattleService/ICaptureService.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index fbae6f178..686500d4a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + diff --git a/src/NosCore.Data/Enumerations/Items/ItemEffectType.cs b/src/NosCore.Data/Enumerations/Items/ItemEffectType.cs index 4964c2471..e9212a6e2 100644 --- a/src/NosCore.Data/Enumerations/Items/ItemEffectType.cs +++ b/src/NosCore.Data/Enumerations/Items/ItemEffectType.cs @@ -14,6 +14,7 @@ public enum ItemEffectType : ushort ApplyHairStyle = 11, Speaker = 15, MarriageProposal = 34, + MateCapture = 100, Undefined = 69, SpCharger = 71, DroppedSpRecharger = 150, diff --git a/src/NosCore.GameObject/Ecs/Extensions/CharacterEntityExtension.cs b/src/NosCore.GameObject/Ecs/Extensions/CharacterEntityExtension.cs index 1e8d1f3b7..faf927262 100644 --- a/src/NosCore.GameObject/Ecs/Extensions/CharacterEntityExtension.cs +++ b/src/NosCore.GameObject/Ecs/Extensions/CharacterEntityExtension.cs @@ -225,9 +225,9 @@ public static IEnumerable GenerateQuicklist(this ICharacterEntity c return pktQs; } - public static RsfiPacket GenerateRsfi(this ICharacterEntity characterEntity) + public static NosCore.Packets.ServerPackets.Player.RsfiPacket GenerateRsfi(this ICharacterEntity characterEntity) { - return new RsfiPacket + return new NosCore.Packets.ServerPackets.Player.RsfiPacket { Act = 1, ActPart = 1, diff --git a/src/NosCore.GameObject/Messaging/Events/EntityCapturedEvent.cs b/src/NosCore.GameObject/Messaging/Events/EntityCapturedEvent.cs new file mode 100644 index 000000000..943fe0706 --- /dev/null +++ b/src/NosCore.GameObject/Messaging/Events/EntityCapturedEvent.cs @@ -0,0 +1,17 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using NosCore.GameObject.Ecs.Interfaces; + +namespace NosCore.GameObject.Messaging.Events +{ + // Published by CaptureService after a successful pet capture. Intentionally + // separate from EntityDiedEvent so capture doesn't inherit the kill pipeline's + // side effects (xp/gold rewards, death bcards, kill-count quest credit). + // Capture-specific quest objectives and any other capture-only reactions + // subscribe to this instead. + public sealed record EntityCapturedEvent(IAliveEntity Victim, IAliveEntity Captor); +} diff --git a/src/NosCore.GameObject/Messaging/Handlers/Quest/OnEntityCapturedHandler.cs b/src/NosCore.GameObject/Messaging/Handlers/Quest/OnEntityCapturedHandler.cs new file mode 100644 index 000000000..f72f74f70 --- /dev/null +++ b/src/NosCore.GameObject/Messaging/Handlers/Quest/OnEntityCapturedHandler.cs @@ -0,0 +1,39 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using NosCore.GameObject.Ecs.Interfaces; +using NosCore.GameObject.Messaging.Events; +using NosCore.GameObject.Services.QuestService; +using Microsoft.Extensions.Logging; + +namespace NosCore.GameObject.Messaging.Handlers.Quest +{ + // Capture counterpart to OnEntityDiedHandler: forwards the captor's capture + // to the quest layer so capture-specific quest objectives (CAPTURE_AND_KEEP / + // CAPTURE_WITHOUT_KEEPING equivalents) can be progressed. + [UsedImplicitly] + public sealed class OnEntityCapturedHandler(IQuestService questService, ILogger logger) + { + [UsedImplicitly] + public async Task Handle(EntityCapturedEvent evt) + { + if (evt.Captor is not ICharacterEntity character) return; + if (evt.Victim is not INonPlayableEntity npc || npc.NpcMonster is null) return; + + try + { + await questService.OnMonsterCapturedAsync(character, npc.NpcMonster).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to progress capture quest for character {CharacterId}", character.VisualId); + } + } + } +} diff --git a/src/NosCore.GameObject/Services/BattleService/BattleService.cs b/src/NosCore.GameObject/Services/BattleService/BattleService.cs index d71808ba2..2ded0e981 100644 --- a/src/NosCore.GameObject/Services/BattleService/BattleService.cs +++ b/src/NosCore.GameObject/Services/BattleService/BattleService.cs @@ -37,6 +37,7 @@ public sealed class BattleService( IMessageBus messageBus, ISessionRegistry sessionRegistry, IClock clock, + ICaptureService captureService, ILogger logger) : IBattleService { // CharacterId (VisualId) → CastId → ReadyAt. Populated by ScheduleCooldownReset @@ -60,6 +61,24 @@ public async Task Hit(IAliveEntity origin, IAliveEntity target, HitArguments arg return; } + // Capture skills branch before damage — vanosilla / OpenNos both suppress + // normal damage and SuPacket for skills carrying a CaptureAnimal bcard. + // A fail still consumes the cooldown via ScheduleCooldownReset below. + if (captureService.IsCaptureSkill(skill)) + { + try + { + await captureService.TryCaptureAsync(origin, target, skill).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogError(ex, "Capture attempt failed: {Attacker} -> {Target}", origin.VisualId, target.VisualId); + } + + ScheduleCooldownReset(origin, skill); + return; + } + var targets = targetResolver.Resolve(origin, target, skill); // Primary target is always first; we pass IsPrimaryTarget so the queue / diff --git a/src/NosCore.GameObject/Services/BattleService/CaptureService.cs b/src/NosCore.GameObject/Services/BattleService/CaptureService.cs new file mode 100644 index 000000000..9c2200521 --- /dev/null +++ b/src/NosCore.GameObject/Services/BattleService/CaptureService.cs @@ -0,0 +1,127 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System; +using System.Linq; +using System.Threading.Tasks; +using NodaTime; +using NosCore.Dao.Interfaces; +using NosCore.Data.Dto; +using NosCore.Data.Enumerations.Buff; +using NosCore.Data.Enumerations.Character; +using NosCore.GameObject.Ecs; +using NosCore.GameObject.Ecs.Interfaces; +using NosCore.GameObject.Messaging.Events; +using NosCore.GameObject.Services.BattleService.Model; +using NosCore.Networking; +using NosCore.Packets.Enumerations; +using NosCore.Packets.ServerPackets.Entities; +using NosCore.Shared.Enumerations; +using NosCore.Shared.Helpers; +using Wolverine; + +namespace NosCore.GameObject.Services.BattleService +{ + // Capture rules mirror vanosilla BCardCaptureHandler + MonsterCaptureEventHandler: + // - target must be a monster at <50 % HP + // - monster.Level <= player.Level + // - guards for raid instance / player dignity skipped here (kept for a follow-up; + // dignity/MapInstanceType don't affect the mechanic enough to block an MVP) + // - rate: 90 % if player < level 20, else 50 % + // - mate level = max(monster.Level - 15, 1) + public sealed class CaptureService(IDao mateDao, IMessageBus messageBus, IClock clock) : ICaptureService + { + private const int CaptureHpThresholdPercent = 50; + private const int LowLevelCaptureRate = 90; + private const int DefaultCaptureRate = 50; + private const int LowLevelCutoff = 20; + private const byte MateStartLevelOffset = 15; + + public bool IsCaptureSkill(SkillInfo skill) => + HasCaptureBCard(skill); + + public async Task TryCaptureAsync(IAliveEntity caster, IAliveEntity target, SkillInfo skill) + { + if (!HasCaptureBCard(skill)) + { + return; + } + + if (caster is not ICharacterEntity character) + { + return; + } + + if (target is not MonsterComponentBundle monster) + { + return; + } + + if (monster.NpcMonster == null || !monster.IsAlive || monster.MapInstance == null) + { + return; + } + + if (monster.NpcMonster.Level > character.Level) + { + return; + } + + var maxHp = monster.NpcMonster.MaxHp; + var hpPercent = maxHp > 0 ? (monster.Hp * 100 / maxHp) : 100; + if (hpPercent >= CaptureHpThresholdPercent) + { + return; + } + + var rate = character.Level < LowLevelCutoff ? LowLevelCaptureRate : DefaultCaptureRate; + if (RandomHelper.Instance.RandomNumber(0, 100) >= rate) + { + return; + } + + var mateLevel = (byte)System.Math.Max(1, monster.NpcMonster.Level - MateStartLevelOffset); + + await mateDao.TryInsertOrUpdateAsync(new MateDto + { + CharacterId = character.CharacterId, + VNum = (short)monster.NpcMonster.NpcMonsterVNum, + MateType = MateType.Pet, + Level = mateLevel, + Loyalty = 1000, + Experience = 0, + Hp = monster.NpcMonster.MaxHp, + Mp = monster.NpcMonster.MaxMp, + IsSummonable = true, + }).ConfigureAwait(false); + + monster.Hp = 0; + monster.IsAlive = false; + monster.HitList.Clear(); + + await monster.MapInstance.SendPacketAsync(new OutPacket + { + VisualType = VisualType.Monster, + VisualId = monster.VisualId + }).ConfigureAwait(false); + + // Capture still vacates the spawn point; re-use the same map-tick respawn + // path MonsterRespawnHandler uses for kills so a captured monster eventually + // reappears at its spawn. Going through ScheduleRespawn directly (rather + // than via EntityDiedEvent) avoids pulling in the kill pipeline's xp/gold + // rewards, death bcards and hunt-quest credit. + var respawnMs = Math.Max(1000, monster.NpcMonster.RespawnTime); + monster.MapInstance.ScheduleRespawn(monster, clock.GetCurrentInstant().Plus(Duration.FromMilliseconds(respawnMs))); + + await messageBus.PublishAsync(new EntityCapturedEvent(monster, character)).ConfigureAwait(false); + } + + private static bool HasCaptureBCard(SkillInfo skill) => + skill.BCards.Any(b => + (BCardType.CardType)b.Type == BCardType.CardType.Capture + && (AdditionalTypes.Capture)b.SubType == AdditionalTypes.Capture.CaptureAnimal); + } +} diff --git a/src/NosCore.GameObject/Services/BattleService/ICaptureService.cs b/src/NosCore.GameObject/Services/BattleService/ICaptureService.cs new file mode 100644 index 000000000..494d5b629 --- /dev/null +++ b/src/NosCore.GameObject/Services/BattleService/ICaptureService.cs @@ -0,0 +1,28 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Threading.Tasks; +using NosCore.GameObject.Ecs.Interfaces; +using NosCore.GameObject.Services.BattleService.Model; + +namespace NosCore.GameObject.Services.BattleService +{ + // Handles skills that carry a `BCardType.Capture` / `CaptureAnimal` bcard. Mirrors + // vanosilla's BCardCaptureHandler + MonsterCaptureEventHandler flow (see + // `_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCaptureHandler.cs`). + public interface ICaptureService + { + // True if `skill` is a capture skill (contains a CaptureAnimal bcard). + // Callers use this to short-circuit the normal damage pipeline. + bool IsCaptureSkill(SkillInfo skill); + + // Evaluates guards, rolls the capture rate, and on success inserts a Mate and + // despawns the monster. No-op (and returns early) if `skill` is not a capture + // skill or the target isn't a monster. Never throws for business-rule violations + // — guard failures just skip the capture so the rest of the pipeline can react. + Task TryCaptureAsync(IAliveEntity caster, IAliveEntity target, SkillInfo skill); + } +} diff --git a/src/NosCore.GameObject/Services/QuestService/IQuestService.cs b/src/NosCore.GameObject/Services/QuestService/IQuestService.cs index dfb152cf2..6ddd2cd21 100644 --- a/src/NosCore.GameObject/Services/QuestService/IQuestService.cs +++ b/src/NosCore.GameObject/Services/QuestService/IQuestService.cs @@ -17,6 +17,12 @@ public interface IQuestService Task RunScriptAsync(ICharacterEntity character); Task RunScriptAsync(ICharacterEntity character, ScriptClientPacket? packet); Task OnMonsterKilledAsync(ICharacterEntity character, NpcMonsterDto mob); + + // Capture-specific quest progression (CAPTURE_AND_KEEP / CAPTURE_WITHOUT_KEEPING + // equivalents). Kept distinct from OnMonsterKilledAsync so capture doesn't + // accidentally credit hunt / kill objectives. + Task OnMonsterCapturedAsync(ICharacterEntity character, NpcMonsterDto mob); + Task AddQuestAsync(ICharacterEntity character, QuestActionType type, short questId); // Flips CompletedOn (if not already), sends the client-side UI packet diff --git a/src/NosCore.GameObject/Services/QuestService/Quest.cs b/src/NosCore.GameObject/Services/QuestService/Quest.cs index 47573fe94..1655892d0 100644 --- a/src/NosCore.GameObject/Services/QuestService/Quest.cs +++ b/src/NosCore.GameObject/Services/QuestService/Quest.cs @@ -42,33 +42,36 @@ public bool AreObjectivesComplete() public QuestSubPacket GenerateQuestSubPacket(bool showDialog) { - var objectives = new List(); - var questCount = 0; - var complete = AreObjectivesComplete(); - foreach (var objective in Quest.QuestObjectives) + var pairs = new (short Cur, short Req)[5]; + var countable = IsCountableObjective(Quest.QuestType); + for (var i = 0; i < Quest.QuestObjectives.Count && i < 5; i++) { - var maxCount = IsCountableObjective(Quest.QuestType) ? (short)(objective.SecondData ?? 0) : (short)0; - var currentCount = IsCountableObjective(Quest.QuestType) + var objective = Quest.QuestObjectives[i]; + var required = countable ? (short)(objective.SecondData ?? 0) : (short)0; + var current = countable && ObjectiveProgress.TryGetValue(objective.QuestObjectiveId, out var c) ? (short)c : (short)0; - objectives.Add(new QuestObjectiveSubPacket - { - CurrentCount = currentCount, - MaxCount = maxCount, - IsFinished = questCount == 0 ? complete : (bool?)null - }); - questCount++; + pairs[i] = (current, required); } return new QuestSubPacket { QuestId = QuestId, - InfoId = QuestId, - GoalType = Quest.QuestType, - ObjectiveCount = 5, - ShowDialog = showDialog, - QuestObjectiveSubPackets = objectives + QuestLineId = QuestId, + QuestType = Quest.QuestType, + Status = (byte)(AreObjectivesComplete() ? 1 : 0), + Objective1Current = pairs[0].Cur, + Objective1Required = pairs[0].Req, + Objective2Current = pairs[1].Cur, + Objective2Required = pairs[1].Req, + Objective3Current = pairs[2].Cur, + Objective3Required = pairs[2].Req, + Objective4Current = pairs[3].Cur, + Objective4Required = pairs[3].Req, + Objective5Current = pairs[4].Cur, + Objective5Required = pairs[4].Req, + QuestToShowInfo = showDialog }; } diff --git a/src/NosCore.GameObject/Services/QuestService/QuestService.cs b/src/NosCore.GameObject/Services/QuestService/QuestService.cs index 46d27511b..2b5d13b10 100644 --- a/src/NosCore.GameObject/Services/QuestService/QuestService.cs +++ b/src/NosCore.GameObject/Services/QuestService/QuestService.cs @@ -403,6 +403,15 @@ public async Task OnMonsterKilledAsync(ICharacterEntity character, NpcMonsterDto } } + // Plumbing for capture-specific quest objectives. No capture QuestType handlers + // exist yet (NosCore has no CAPTURE_AND_KEEP / CAPTURE_WITHOUT_KEEPING equivalent + // in QuestType); when they land, they'll implement a matching handler method + // and this loop will route captures to them exactly like the kill loop above. + public Task OnMonsterCapturedAsync(ICharacterEntity character, NpcMonsterDto mob) + { + return Task.CompletedTask; + } + public async Task CompleteQuestAsync(ICharacterEntity character, CharacterQuest quest) { quest.ObjectivesCompletedOn ??= clock.GetCurrentInstant(); diff --git a/src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs b/src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs index 7d9a11436..8e4367fc4 100644 --- a/src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs +++ b/src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs @@ -20,7 +20,8 @@ public class SetHairStylePacketHandler : PacketHandler, IWor { public override async Task ExecuteAsync(SetHairStylePacket packet, ClientSession session) { - if (!Enum.IsDefined(typeof(HairStyleType), packet.Style)) + if (!Enum.IsDefined(typeof(HairStyleType), packet.Style) || + packet.Style > (byte)HairStyleType.HairStyleB) { return; } diff --git a/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs b/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs index e98cc4508..ae1bb94eb 100644 --- a/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs +++ b/test/NosCore.GameObject.Tests/Services/BattleService/BattleServiceTests.cs @@ -50,6 +50,7 @@ public void Setup() _bus.Object, new Mock().Object, NodaTime.SystemClock.Instance, + new Mock().Object, NullLogger.Instance); } From 389a7e1577c381c4d14f2731ed3b20af316c5cdc Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 02:28:41 +1200 Subject: [PATCH 07/14] chore: bump OpenTelemetry to patched versions (GHSA-g94r-2vxg-569j, GHSA-mr8r-92fq-pj8p) Two moderate-severity advisories surfaced by NuGet audit against the 1.15.x line we shipped in #2085: - OpenTelemetry.Api 1.15.2 -> GHSA-g94r-2vxg-569j - OpenTelemetry.Exporter.OpenTelemetryProtocol 1.15.2 -> GHSA-mr8r-92fq-pj8p Bumps: - OpenTelemetry.Exporter.OpenTelemetryProtocol 1.15.2 -> 1.15.3 - OpenTelemetry.Extensions.Hosting 1.15.2 -> 1.15.3 - OpenTelemetry.Instrumentation.AspNetCore 1.15.1 -> 1.15.2 - OpenTelemetry.Instrumentation.EntityFrameworkCore 1.15.0-beta.1 -> 1.15.1-beta.1 - OpenTelemetry.Instrumentation.Http 1.15.0 -> 1.15.1 - OpenTelemetry.Instrumentation.Runtime 1.15.0 -> 1.15.1 Build clean with 0 vulnerability warnings. Full test suite green. Co-Authored-By: Claude Opus 4.7 (1M context) --- Directory.Packages.props | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 686500d4a..b6f685f0f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -51,12 +51,12 @@ - - - - - - + + + + + + From f105793102180b640ac5c5a8c6463eb141b44e8d Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 02:46:03 +1200 Subject: [PATCH 08/14] chore: add MateCapture item_effect_type migration Aligns the Postgres enum with ItemEffectType.MateCapture introduced during the pet-capture work. Current capture path is skill-based (BCardType.Capture / CaptureAnimal) but the enum value is kept so the label is available if we later wire item-based capture cards the way vanilla NosTale does. Runs a single AlterDatabase with the new enum label appended; no column changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...44528_AddMateCaptureItemEffect.Designer.cs | 4151 +++++++++++++++++ ...20260424144528_AddMateCaptureItemEffect.cs | 134 + .../Migrations/NosCoreContextModelSnapshot.cs | 2 +- 3 files changed, 4286 insertions(+), 1 deletion(-) create mode 100644 src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.Designer.cs create mode 100644 src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.cs diff --git a/src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.Designer.cs b/src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.Designer.cs new file mode 100644 index 000000000..26a3eb8f1 --- /dev/null +++ b/src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.Designer.cs @@ -0,0 +1,4151 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using NosCore.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NosCore.Database.Migrations +{ + [DbContext(typeof(NosCoreContext))] + [Migration("20260424144528_AddMateCaptureItemEffect")] + partial class AddMateCaptureItemEffect + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "audit_log_type", new[] { "account_creation", "character_creation", "email_update" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "authority_type", new[] { "user", "moderator", "game_master", "administrator", "root", "closed", "banned", "unconfirmed" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "character_class_type", new[] { "adventurer", "swordsman", "archer", "mage", "martial_artist" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "character_relation_type", new[] { "friend", "hidden_spouse", "spouse", "blocked" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "character_state", new[] { "active", "inactive" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "element_type", new[] { "neutral", "fire", "water", "light", "dark" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "equipment_type", new[] { "main_weapon", "armor", "hat", "gloves", "boots", "secondary_weapon", "necklace", "ring", "bracelet", "mask", "fairy", "amulet", "sp", "costume_suit", "costume_hat", "weapon_skin", "wing_skin" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "family_authority", new[] { "head", "assistant", "manager", "member" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "family_authority_type", new[] { "none", "put", "all" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "family_log_type", new[] { "daily_message", "raid_won", "rainbow_battle", "family_xp", "family_level_up", "level_up", "item_upgraded", "right_changed", "authority_changed", "family_managed", "user_managed", "ware_house_added", "ware_house_removed" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "family_member_rank", new[] { "nothing", "old_uncle", "old_aunt", "father", "mother", "uncle", "aunt", "brother", "sister", "spouse", "brother2", "sister2", "old_son", "old_daugter", "middle_son", "middle_daughter", "young_son", "young_daugter", "old_little_son", "old_little_daughter", "little_son", "little_daughter", "middle_little_son", "middle_little_daugter" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "gender_type", new[] { "male", "female" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "hair_color_type", new[] { "dark_purple", "yellow", "blue", "purple", "orange", "brown", "green", "dark_grey", "light_blue", "pink_red", "light_yellow", "light_pink", "light_green", "light_grey", "sky_blue", "black", "dark_orange", "dark_orange_variant2", "dark_orange_variant3", "dark_orange_variant4", "dark_orange_variant5", "dark_orange_variant6", "light_orange", "light_light_orange", "light_light_light_orange", "light_light_light_light_orange", "super_light_orange", "dark_yellow", "light_light_yellow", "kaki_yellow", "super_light_yellow", "super_light_yellow2", "super_light_yellow3", "little_dark_yellow", "yellow_variant", "yellow_variant1", "yellow_variant2", "yellow_variant3", "yellow_variant4", "yellow_variant5", "yellow_variant6", "yellow_variant7", "yellow_variant8", "yellow_variant9", "green_variant", "green_variant1", "dark_green_variant", "green_more_dark_variant", "green_variant2", "green_variant3", "green_variant4", "green_variant5", "green_variant6", "green_variant7", "green_variant8", "green_variant9", "green_variant10", "green_variant11", "green_variant12", "green_variant13", "green_variant14", "green_variant15", "green_variant16", "green_variant17", "green_variant18", "green_variant19", "green_variant20", "light_blue_variant1", "light_blue_variant2", "light_blue_variant3", "light_blue_variant4", "light_blue_variant5", "light_blue_variant6", "light_blue_variant7", "light_blue_variant8", "light_blue_variant9", "light_blue_variant10", "light_blue_variant11", "light_blue_variant12", "light_blue_variant13", "dark_black", "light_blue_variant14", "light_blue_variant15", "light_blue_variant16", "light_blue_variant17", "blue_variant", "blue_variant_dark", "blue_variant_dark_dark", "blue_variant_dark_dark2", "flash_blue", "flash_blue_dark", "flash_blue_dark2", "flash_blue_dark3", "flash_blue_dark4", "flash_blue_dark5", "flash_blue_dark6", "flash_blue_dark7", "flash_blue_dark8", "flash_blue_dark9", "white", "flash_blue_dark10", "flash_blue1", "flash_blue2", "flash_blue3", "flash_blue4", "flash_blue5", "flash_purple", "flash_light_purple", "flash_light_purple2", "flash_light_purple3", "flash_light_purple4", "flash_light_purple5", "light_purple", "purple_variant1", "purple_variant2", "purple_variant3", "purple_variant4", "purple_variant5", "purple_variant6", "purple_variant7", "purple_variant8", "purple_variant9", "purple_variant10", "purple_variant11", "purple_variant12", "purple_variant13", "purple_variant14", "purple_variant15" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "hair_style_type", new[] { "hair_style_a", "hair_style_b", "hair_style_c", "hair_style_d", "no_hair" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_effect_type", new[] { "no_effect", "teleport", "apply_hair_die", "apply_hair_style", "speaker", "marriage_proposal", "undefined", "sp_charger", "mate_capture", "dropped_sp_recharger", "premium_sp_recharger", "crafted_sp_recharger", "specialist_medal", "apply_skin_partner", "change_gender", "point_initialisation", "sealed_tarot_card", "tarot_card", "red_amulet", "blue_amulet", "reinforcement_amulet", "heroic", "random_heroic", "attack_amulet", "defense_amulet", "speed_booster", "box_effect", "vehicle", "gold_nos_merchant_upgrade", "silver_nos_merchant_upgrade", "inventory_upgrade", "pet_space_upgrade", "pet_basket_upgrade", "pet_backpack_upgrade", "inventory_ticket_upgrade", "buff_potions", "marriage_separation" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_type", new[] { "weapon", "armor", "fashion", "jewelery", "specialist", "box", "shell", "main", "upgrade", "production", "map", "special", "potion", "event", "title", "quest1", "sell", "food", "snack", "magical", "part", "teacher", "ammo", "quest2", "house", "garden", "minigame", "terrace", "miniland_theme" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "mate_type", new[] { "partner", "pet" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "miniland_state", new[] { "open", "private", "lock" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "monster_type", new[] { "unknown", "partner", "npc", "well", "portal", "boss", "elite", "peapod", "special", "gem_space_time" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "noscore_pocket_type", new[] { "equipment", "main", "etc", "miniland", "specialist", "costume", "wear", "mount", "raid" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "penalty_type", new[] { "muted", "banned", "block_exp", "block_f_exp", "block_rep", "warning" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "portal_type", new[] { "ts_normal", "closed", "open", "miniland", "ts_end", "ts_end_closed", "exit", "exit_closed", "raid", "effect", "blue_raid", "dark_raid", "time_space", "shop_teleport", "map_portal" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "quest_type", new[] { "hunt", "special_collect", "collect_in_raid", "brings", "capture_without_getting_the_monster", "capture", "times_space", "product", "number_of_kill", "target_reput", "ts_point", "dialog1", "collect_in_ts", "required", "wear", "needed", "collect", "transmit_gold", "go_to", "collect_map_entity", "use", "dialog2", "un_know", "inspect", "win_raid", "flower_quest" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "region_type", new[] { "en", "de", "fr", "it", "pl", "es", "ru", "cs", "tr" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "scripted_instance_type", new[] { "time_space", "raid", "raid_act4" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "static_bonus_type", new[] { "bazaar_medal_gold", "bazaar_medal_silver", "back_pack", "pet_basket", "pet_back_pack", "inventory_ticket_upgrade" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "teleporter_type", new[] { "teleporter", "teleporter_on_map" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "warehouse_type", new[] { "warehouse", "family_ware_house", "pet_warehouse" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NosCore.Database.Entities.Account", b => + { + b.Property("AccountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AccountId")); + + b.Property("Authority") + .HasColumnType("smallint"); + + b.Property("BankMoney") + .HasColumnType("bigint"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ItemShopMoney") + .HasColumnType("bigint"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("MfaSecret") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NewAuthPassword") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NewAuthSalt") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Password") + .HasMaxLength(255) + .IsUnicode(false) + .HasColumnType("character varying(255)"); + + b.Property("RegistrationIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("VerificationToken") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("AccountId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Act", b => + { + b.Property("ActId") + .HasColumnType("smallint"); + + b.Property("Scene") + .HasColumnType("smallint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("ActId"); + + b.ToTable("Act"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ActPart", b => + { + b.Property("ActPartId") + .HasColumnType("smallint"); + + b.Property("ActId") + .HasColumnType("smallint"); + + b.Property("ActPartNumber") + .HasColumnType("smallint"); + + b.Property("MaxTs") + .HasColumnType("smallint"); + + b.HasKey("ActPartId"); + + b.HasIndex("ActId"); + + b.ToTable("ActPart"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.AuditLog", b => + { + b.Property("AuditId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogType") + .HasColumnType("integer"); + + b.Property("TargetId") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("AuditId"); + + b.ToTable("AuditLog"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.BCard", b => + { + b.Property("BCardId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BCardId")); + + b.Property("CardId") + .HasColumnType("smallint"); + + b.Property("CastType") + .HasColumnType("smallint"); + + b.Property("FirstData") + .HasColumnType("integer"); + + b.Property("IsLevelDivided") + .HasColumnType("boolean"); + + b.Property("IsLevelScaled") + .HasColumnType("boolean"); + + b.Property("ItemVNum") + .HasColumnType("smallint"); + + b.Property("NpcMonsterVNum") + .HasColumnType("smallint"); + + b.Property("SecondData") + .HasColumnType("integer"); + + b.Property("SkillVNum") + .HasColumnType("smallint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("SubType") + .HasColumnType("smallint"); + + b.Property("ThirdData") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("BCardId"); + + b.HasIndex("CardId"); + + b.HasIndex("ItemVNum"); + + b.HasIndex("NpcMonsterVNum"); + + b.HasIndex("SkillVNum"); + + b.ToTable("BCard"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.BazaarItem", b => + { + b.Property("BazaarItemId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BazaarItemId")); + + b.Property("Amount") + .HasColumnType("smallint"); + + b.Property("DateStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("smallint"); + + b.Property("IsPackage") + .HasColumnType("boolean"); + + b.Property("ItemInstanceId") + .HasColumnType("uuid"); + + b.Property("MedalUsed") + .HasColumnType("boolean"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("SellerId") + .HasColumnType("bigint"); + + b.HasKey("BazaarItemId"); + + b.HasIndex("ItemInstanceId"); + + b.HasIndex("SellerId"); + + b.ToTable("BazaarItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Card", b => + { + b.Property("CardId") + .HasColumnType("smallint"); + + b.Property("BuffType") + .HasColumnType("smallint"); + + b.Property("Delay") + .HasColumnType("integer"); + + b.Property("Duration") + .HasColumnType("integer"); + + b.Property("EffectId") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Propability") + .HasColumnType("smallint"); + + b.Property("TimeoutBuff") + .HasColumnType("smallint"); + + b.Property("TimeoutBuffChance") + .HasColumnType("smallint"); + + b.HasKey("CardId"); + + b.ToTable("Card"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Character", b => + { + b.Property("CharacterId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CharacterId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Act4Dead") + .HasColumnType("integer"); + + b.Property("Act4Kill") + .HasColumnType("integer"); + + b.Property("Act4Points") + .HasColumnType("integer"); + + b.Property("ArenaWinner") + .HasColumnType("integer"); + + b.Property("Biography") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BuffBlocked") + .HasColumnType("boolean"); + + b.Property("Class") + .HasColumnType("smallint"); + + b.Property("Compliment") + .HasColumnType("smallint"); + + b.Property("CurrentScriptId") + .HasColumnType("uuid"); + + b.Property("Dignity") + .HasColumnType("smallint"); + + b.Property("Elo") + .HasColumnType("integer"); + + b.Property("EmoticonsBlocked") + .HasColumnType("boolean"); + + b.Property("ExchangeBlocked") + .HasColumnType("boolean"); + + b.Property("Faction") + .HasColumnType("smallint"); + + b.Property("FamilyRequestBlocked") + .HasColumnType("boolean"); + + b.Property("FriendRequestBlocked") + .HasColumnType("boolean"); + + b.Property("Gender") + .HasColumnType("smallint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("GroupRequestBlocked") + .HasColumnType("boolean"); + + b.Property("HairColor") + .HasColumnType("smallint"); + + b.Property("HairStyle") + .HasColumnType("smallint"); + + b.Property("HeroChatBlocked") + .HasColumnType("boolean"); + + b.Property("HeroLevel") + .HasColumnType("smallint"); + + b.Property("HeroXp") + .HasColumnType("bigint"); + + b.Property("Hp") + .HasColumnType("integer"); + + b.Property("HpBlocked") + .HasColumnType("boolean"); + + b.Property("JobLevel") + .HasColumnType("smallint"); + + b.Property("JobLevelXp") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("LevelXp") + .HasColumnType("bigint"); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.Property("MasterPoints") + .HasColumnType("integer"); + + b.Property("MasterTicket") + .HasColumnType("integer"); + + b.Property("MaxMateCount") + .HasColumnType("smallint"); + + b.Property("MinilandInviteBlocked") + .HasColumnType("boolean"); + + b.Property("MouseAimLock") + .HasColumnType("boolean"); + + b.Property("Mp") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .IsUnicode(false) + .HasColumnType("character varying(255)"); + + b.Property("Prefix") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("QuickGetUp") + .HasColumnType("boolean"); + + b.Property("RagePoint") + .HasColumnType("bigint"); + + b.Property("Reput") + .HasColumnType("bigint"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.Property("ShouldRename") + .HasColumnType("boolean"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("SpAdditionPoint") + .HasColumnType("integer"); + + b.Property("SpPoint") + .HasColumnType("integer"); + + b.Property("State") + .HasColumnType("smallint"); + + b.Property("TalentLose") + .HasColumnType("integer"); + + b.Property("TalentSurrender") + .HasColumnType("integer"); + + b.Property("TalentWin") + .HasColumnType("integer"); + + b.Property("WhisperBlocked") + .HasColumnType("boolean"); + + b.HasKey("CharacterId"); + + b.HasIndex("AccountId"); + + b.HasIndex("CurrentScriptId"); + + b.HasIndex("MapId"); + + b.ToTable("Character"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterActPart", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActPartId") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ActPartId"); + + b.HasIndex("CharacterId"); + + b.ToTable("CharacterActPart"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterQuest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CompletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("QuestId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("QuestId"); + + b.ToTable("CharacterQuest"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterQuestObjective", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CharacterQuestId") + .HasColumnType("uuid"); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("QuestObjectiveId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("QuestObjectiveId"); + + b.HasIndex("CharacterQuestId", "QuestObjectiveId") + .IsUnique(); + + b.ToTable("CharacterQuestObjective"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterRelation", b => + { + b.Property("CharacterRelationId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("RelatedCharacterId") + .HasColumnType("bigint"); + + b.Property("RelationType") + .HasColumnType("smallint"); + + b.HasKey("CharacterRelationId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("RelatedCharacterId"); + + b.ToTable("CharacterRelation"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterSkill", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("SkillVNum") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("SkillVNum"); + + b.ToTable("CharacterSkill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Combo", b => + { + b.Property("ComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ComboId")); + + b.Property("Animation") + .HasColumnType("smallint"); + + b.Property("Effect") + .HasColumnType("smallint"); + + b.Property("Hit") + .HasColumnType("smallint"); + + b.Property("SkillVNum") + .HasColumnType("smallint"); + + b.HasKey("ComboId"); + + b.HasIndex("SkillVNum"); + + b.ToTable("Combo"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Drop", b => + { + b.Property("DropId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DropId")); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("DropChance") + .HasColumnType("integer"); + + b.Property("MapTypeId") + .HasColumnType("smallint"); + + b.Property("MonsterVNum") + .HasColumnType("smallint"); + + b.Property("VNum") + .HasColumnType("smallint"); + + b.HasKey("DropId"); + + b.HasIndex("MapTypeId"); + + b.HasIndex("MonsterVNum"); + + b.HasIndex("VNum"); + + b.ToTable("Drop"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.EquipmentOption", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("Value") + .HasColumnType("integer"); + + b.Property("WearableInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WearableInstanceId"); + + b.ToTable("EquipmentOption"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Family", b => + { + b.Property("FamilyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FamilyId")); + + b.Property("FamilyExperience") + .HasColumnType("integer"); + + b.Property("FamilyFaction") + .HasColumnType("smallint"); + + b.Property("FamilyHeadGender") + .HasColumnType("smallint"); + + b.Property("FamilyLevel") + .HasColumnType("smallint"); + + b.Property("FamilyMessage") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ManagerAuthorityType") + .HasColumnType("integer"); + + b.Property("ManagerCanGetHistory") + .HasColumnType("boolean"); + + b.Property("ManagerCanInvite") + .HasColumnType("boolean"); + + b.Property("ManagerCanNotice") + .HasColumnType("boolean"); + + b.Property("ManagerCanShout") + .HasColumnType("boolean"); + + b.Property("MaxSize") + .HasColumnType("smallint"); + + b.Property("MemberAuthorityType") + .HasColumnType("integer"); + + b.Property("MemberCanGetHistory") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WarehouseSize") + .HasColumnType("smallint"); + + b.HasKey("FamilyId"); + + b.ToTable("Family"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.FamilyCharacter", b => + { + b.Property("FamilyCharacterId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FamilyCharacterId")); + + b.Property("Authority") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("DailyMessage") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Experience") + .HasColumnType("integer"); + + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.HasKey("FamilyCharacterId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("FamilyId"); + + b.ToTable("FamilyCharacter"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.FamilyLog", b => + { + b.Property("FamilyLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FamilyLogId")); + + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("FamilyLogData") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FamilyLogType") + .HasColumnType("smallint"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("FamilyLogId"); + + b.HasIndex("FamilyId"); + + b.ToTable("FamilyLog"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NActDesc", b => + { + b.Property("I18NActDescId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NActDescId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NActDescId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NActDesc"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NBCard", b => + { + b.Property("I18NbCardId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NbCardId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NbCardId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NBCard"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NCard", b => + { + b.Property("I18NCardId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NCardId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NCardId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NCard"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NItem", b => + { + b.Property("I18NItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NItemId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NItemId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NMapIdData", b => + { + b.Property("I18NMapIdDataId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NMapIdDataId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NMapIdDataId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NMapIdData"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NMapPointData", b => + { + b.Property("I18NMapPointDataId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NMapPointDataId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NMapPointDataId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NMapPointData"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NNpcMonster", b => + { + b.Property("I18NNpcMonsterId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NNpcMonsterId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NNpcMonsterId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NNpcMonster"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NNpcMonsterTalk", b => + { + b.Property("I18NNpcMonsterTalkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NNpcMonsterTalkId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NNpcMonsterTalkId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NNpcMonsterTalk"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NQuest", b => + { + b.Property("I18NQuestId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NQuestId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NQuestId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NQuest"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.I18NSkill", b => + { + b.Property("I18NSkillId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("I18NSkillId")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionType") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("I18NSkillId"); + + b.HasIndex("Key", "RegionType") + .IsUnique(); + + b.ToTable("I18NSkill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.InventoryItemInstance", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("ItemInstanceId") + .HasColumnType("uuid"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemInstanceId"); + + b.HasIndex("CharacterId", "Slot", "Type") + .IsUnique(); + + b.ToTable("InventoryItemInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Item", b => + { + b.Property("VNum") + .HasColumnType("smallint"); + + b.Property("BasicUpgrade") + .HasColumnType("smallint"); + + b.Property("CellonLvl") + .HasColumnType("smallint"); + + b.Property("Class") + .HasColumnType("smallint"); + + b.Property("CloseDefence") + .HasColumnType("smallint"); + + b.Property("Color") + .HasColumnType("smallint"); + + b.Property("Concentrate") + .HasColumnType("smallint"); + + b.Property("CriticalLuckRate") + .HasColumnType("smallint"); + + b.Property("CriticalRate") + .HasColumnType("smallint"); + + b.Property("DamageMaximum") + .HasColumnType("smallint"); + + b.Property("DamageMinimum") + .HasColumnType("smallint"); + + b.Property("DarkElement") + .HasColumnType("smallint"); + + b.Property("DarkResistance") + .HasColumnType("smallint"); + + b.Property("DefenceDodge") + .HasColumnType("smallint"); + + b.Property("DistanceDefence") + .HasColumnType("smallint"); + + b.Property("DistanceDefenceDodge") + .HasColumnType("smallint"); + + b.Property("Effect") + .HasColumnType("integer"); + + b.Property("EffectValue") + .HasColumnType("integer"); + + b.Property("Element") + .HasColumnType("integer"); + + b.Property("ElementRate") + .HasColumnType("smallint"); + + b.Property("EquipmentSlot") + .HasColumnType("smallint"); + + b.Property("FireElement") + .HasColumnType("smallint"); + + b.Property("FireResistance") + .HasColumnType("smallint"); + + b.Property("Flag1") + .HasColumnType("boolean"); + + b.Property("Flag2") + .HasColumnType("boolean"); + + b.Property("Flag3") + .HasColumnType("boolean"); + + b.Property("Flag4") + .HasColumnType("boolean"); + + b.Property("Flag6") + .HasColumnType("boolean"); + + b.Property("Flag7") + .HasColumnType("boolean"); + + b.Property("Flag8") + .HasColumnType("boolean"); + + b.Property("Flag9") + .HasColumnType("boolean"); + + b.Property("Height") + .HasColumnType("smallint"); + + b.Property("HitRate") + .HasColumnType("smallint"); + + b.Property("Hp") + .HasColumnType("smallint"); + + b.Property("HpRegeneration") + .HasColumnType("smallint"); + + b.Property("IsColored") + .HasColumnType("boolean"); + + b.Property("IsConsumable") + .HasColumnType("boolean"); + + b.Property("IsDroppable") + .HasColumnType("boolean"); + + b.Property("IsHeroic") + .HasColumnType("boolean"); + + b.Property("IsMinilandActionable") + .HasColumnType("boolean"); + + b.Property("IsSoldable") + .HasColumnType("boolean"); + + b.Property("IsTradable") + .HasColumnType("boolean"); + + b.Property("IsWarehouse") + .HasColumnType("boolean"); + + b.Property("ItemSubType") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("smallint"); + + b.Property("ItemValidTime") + .HasColumnType("bigint"); + + b.Property("LevelJobMinimum") + .HasColumnType("smallint"); + + b.Property("LevelMinimum") + .HasColumnType("smallint"); + + b.Property("LightElement") + .HasColumnType("smallint"); + + b.Property("LightResistance") + .HasColumnType("smallint"); + + b.Property("MagicDefence") + .HasColumnType("smallint"); + + b.Property("MaxCellon") + .HasColumnType("smallint"); + + b.Property("MaxCellonLvl") + .HasColumnType("smallint"); + + b.Property("MaxElementRate") + .HasColumnType("smallint"); + + b.Property("MaximumAmmo") + .HasColumnType("smallint"); + + b.Property("MinilandObjectPoint") + .HasColumnType("integer"); + + b.Property("MoreHp") + .HasColumnType("smallint"); + + b.Property("MoreMp") + .HasColumnType("smallint"); + + b.Property("Morph") + .HasColumnType("smallint"); + + b.Property("Mp") + .HasColumnType("smallint"); + + b.Property("MpRegeneration") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PvpDefence") + .HasColumnType("smallint"); + + b.Property("PvpStrength") + .HasColumnType("smallint"); + + b.Property("ReduceOposantResistance") + .HasColumnType("smallint"); + + b.Property("ReputPrice") + .HasColumnType("bigint"); + + b.Property("ReputationMinimum") + .HasColumnType("smallint"); + + b.Property("RequireBinding") + .HasColumnType("boolean"); + + b.Property("SecondMorph") + .HasColumnType("smallint"); + + b.Property("SecondaryElement") + .HasColumnType("integer"); + + b.Property("Sex") + .HasColumnType("smallint"); + + b.Property("SpType") + .HasColumnType("smallint"); + + b.Property("Speed") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitDelay") + .HasColumnType("smallint"); + + b.Property("WaterElement") + .HasColumnType("smallint"); + + b.Property("WaterResistance") + .HasColumnType("smallint"); + + b.Property("Width") + .HasColumnType("smallint"); + + b.HasKey("VNum"); + + b.ToTable("Item"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ItemInstance", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("smallint"); + + b.Property("BoundCharacterId") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("Design") + .HasColumnType("smallint"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("character varying(21)"); + + b.Property("DurabilityPoint") + .HasColumnType("integer"); + + b.Property("ItemDeleteTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemVNum") + .HasColumnType("smallint"); + + b.Property("Rare") + .HasColumnType("smallint"); + + b.Property("Upgrade") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("BoundCharacterId"); + + b.HasIndex("ItemVNum"); + + b.ToTable("ItemInstance"); + + b.HasDiscriminator("Discriminator").HasValue("ItemInstance"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Mail", b => + { + b.Property("MailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MailId")); + + b.Property("Armor") + .HasColumnType("smallint"); + + b.Property("CostumeHat") + .HasColumnType("smallint"); + + b.Property("CostumeSuit") + .HasColumnType("smallint"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Fairy") + .HasColumnType("smallint"); + + b.Property("Hat") + .HasColumnType("smallint"); + + b.Property("IsOpened") + .HasColumnType("boolean"); + + b.Property("IsSenderCopy") + .HasColumnType("boolean"); + + b.Property("ItemInstanceId") + .HasColumnType("uuid"); + + b.Property("MainWeapon") + .HasColumnType("smallint"); + + b.Property("Mask") + .HasColumnType("smallint"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("SecondaryWeapon") + .HasColumnType("smallint"); + + b.Property("SenderCharacterClass") + .HasColumnType("smallint"); + + b.Property("SenderGender") + .HasColumnType("smallint"); + + b.Property("SenderHairColor") + .HasColumnType("smallint"); + + b.Property("SenderHairStyle") + .HasColumnType("smallint"); + + b.Property("SenderId") + .HasColumnType("bigint"); + + b.Property("SenderMorphId") + .HasColumnType("smallint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WeaponSkin") + .HasColumnType("smallint"); + + b.Property("WingSkin") + .HasColumnType("smallint"); + + b.HasKey("MailId"); + + b.HasIndex("ItemInstanceId"); + + b.HasIndex("ReceiverId"); + + b.HasIndex("SenderId"); + + b.ToTable("Mail"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Map", b => + { + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Music") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ShopAllowed") + .HasColumnType("boolean"); + + b.HasKey("MapId"); + + b.ToTable("Map"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapMonster", b => + { + b.Property("MapMonsterId") + .HasColumnType("integer"); + + b.Property("Direction") + .HasColumnType("smallint"); + + b.Property("IsDisabled") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.Property("VNum") + .HasColumnType("smallint"); + + b.HasKey("MapMonsterId"); + + b.HasIndex("MapId"); + + b.HasIndex("VNum"); + + b.ToTable("MapMonster"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapNpc", b => + { + b.Property("MapNpcId") + .HasColumnType("integer"); + + b.Property("Dialog") + .HasColumnType("smallint"); + + b.Property("Direction") + .HasColumnType("smallint"); + + b.Property("Effect") + .HasColumnType("smallint"); + + b.Property("EffectDelay") + .HasColumnType("smallint"); + + b.Property("IsDisabled") + .HasColumnType("boolean"); + + b.Property("IsMoving") + .HasColumnType("boolean"); + + b.Property("IsSitting") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.Property("VNum") + .HasColumnType("smallint"); + + b.HasKey("MapNpcId"); + + b.HasIndex("Dialog"); + + b.HasIndex("MapId"); + + b.HasIndex("VNum"); + + b.ToTable("MapNpc"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapType", b => + { + b.Property("MapTypeId") + .HasColumnType("smallint"); + + b.Property("MapTypeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PotionDelay") + .HasColumnType("smallint"); + + b.Property("RespawnMapTypeId") + .HasColumnType("bigint"); + + b.Property("ReturnMapTypeId") + .HasColumnType("bigint"); + + b.HasKey("MapTypeId"); + + b.HasIndex("RespawnMapTypeId"); + + b.HasIndex("ReturnMapTypeId"); + + b.ToTable("MapType"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapTypeMap", b => + { + b.Property("MapTypeMapId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapTypeMapId")); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("MapTypeId") + .HasColumnType("smallint"); + + b.HasKey("MapTypeMapId"); + + b.HasIndex("MapTypeId"); + + b.HasIndex("MapId", "MapTypeId") + .IsUnique(); + + b.ToTable("MapTypeMap"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Mate", b => + { + b.Property("MateId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MateId")); + + b.Property("Attack") + .HasColumnType("smallint"); + + b.Property("CanPickUp") + .HasColumnType("boolean"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("Defence") + .HasColumnType("smallint"); + + b.Property("Direction") + .HasColumnType("smallint"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("Hp") + .HasColumnType("integer"); + + b.Property("IsSummonable") + .HasColumnType("boolean"); + + b.Property("IsTeamMember") + .HasColumnType("boolean"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Loyalty") + .HasColumnType("smallint"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.Property("MateType") + .HasColumnType("smallint"); + + b.Property("Mp") + .HasColumnType("integer"); + + b.Property("Name") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Skin") + .HasColumnType("smallint"); + + b.Property("VNum") + .HasColumnType("smallint"); + + b.HasKey("MateId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("VNum"); + + b.ToTable("Mate"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Miniland", b => + { + b.Property("MinilandId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DailyVisitCount") + .HasColumnType("integer"); + + b.Property("MinilandMessage") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("MinilandPoint") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("smallint"); + + b.Property("VisitCount") + .HasColumnType("integer"); + + b.Property("WelcomeMusicInfo") + .HasColumnType("smallint"); + + b.HasKey("MinilandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Miniland"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MinilandObject", b => + { + b.Property("MinilandObjectId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("InventoryItemInstanceId") + .HasColumnType("uuid"); + + b.Property("Level1BoxAmount") + .HasColumnType("smallint"); + + b.Property("Level2BoxAmount") + .HasColumnType("smallint"); + + b.Property("Level3BoxAmount") + .HasColumnType("smallint"); + + b.Property("Level4BoxAmount") + .HasColumnType("smallint"); + + b.Property("Level5BoxAmount") + .HasColumnType("smallint"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.HasKey("MinilandObjectId"); + + b.HasIndex("InventoryItemInstanceId"); + + b.ToTable("MinilandObject"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.NpcMonster", b => + { + b.Property("NpcMonsterVNum") + .HasColumnType("smallint"); + + b.Property("AlwaysActive") + .HasColumnType("boolean"); + + b.Property("AmountRequired") + .HasColumnType("smallint"); + + b.Property("AttackClass") + .HasColumnType("smallint"); + + b.Property("AttackUpgrade") + .HasColumnType("smallint"); + + b.Property("BasicArea") + .HasColumnType("smallint"); + + b.Property("BasicCooldown") + .HasColumnType("smallint"); + + b.Property("BasicHitChance") + .HasColumnType("smallint"); + + b.Property("BasicRange") + .HasColumnType("smallint"); + + b.Property("BasicSkill") + .HasColumnType("smallint"); + + b.Property("CModeVNum") + .HasColumnType("smallint"); + + b.Property("CanCatch") + .HasColumnType("boolean"); + + b.Property("CanCollect") + .HasColumnType("boolean"); + + b.Property("CanOnlyBeDmgedByJajamaruLastSkill") + .HasColumnType("boolean"); + + b.Property("CanWalk") + .HasColumnType("boolean"); + + b.Property("CantDebuff") + .HasColumnType("boolean"); + + b.Property("CantTargetInfo") + .HasColumnType("boolean"); + + b.Property("CantVoke") + .HasColumnType("boolean"); + + b.Property("CellMinRange") + .HasColumnType("smallint"); + + b.Property("CellSize") + .HasColumnType("smallint"); + + b.Property("CloseDefence") + .HasColumnType("smallint"); + + b.Property("Concentrate") + .HasColumnType("smallint"); + + b.Property("CriticalChance") + .HasColumnType("smallint"); + + b.Property("CriticalRate") + .HasColumnType("smallint"); + + b.Property("DamageMaximum") + .HasColumnType("smallint"); + + b.Property("DamageMinimum") + .HasColumnType("smallint"); + + b.Property("DarkResistance") + .HasColumnType("smallint"); + + b.Property("DashSpeed") + .HasColumnType("smallint"); + + b.Property("DefenceDodge") + .HasColumnType("smallint"); + + b.Property("DefenceUpgrade") + .HasColumnType("smallint"); + + b.Property("DisappearAfterHitting") + .HasColumnType("boolean"); + + b.Property("DisappearAfterSeconds") + .HasColumnType("boolean"); + + b.Property("DisappearAfterSecondsMana") + .HasColumnType("boolean"); + + b.Property("DistanceDefence") + .HasColumnType("smallint"); + + b.Property("DistanceDefenceDodge") + .HasColumnType("smallint"); + + b.Property("DontDrainHpAfterSeconds") + .HasColumnType("boolean"); + + b.Property("EffectIdConstantly") + .HasColumnType("smallint"); + + b.Property("EffectIdOnDeath") + .HasColumnType("smallint"); + + b.Property("Element") + .HasColumnType("smallint"); + + b.Property("ElementRate") + .HasColumnType("smallint"); + + b.Property("FireResistance") + .HasColumnType("smallint"); + + b.Property("GiveDamagePercentage") + .HasColumnType("integer"); + + b.Property("GroupAttack") + .HasColumnType("smallint"); + + b.Property("HasDash") + .HasColumnType("boolean"); + + b.Property("HasMode") + .HasColumnType("boolean"); + + b.Property("HeroLevel") + .HasColumnType("smallint"); + + b.Property("HeroXp") + .HasColumnType("integer"); + + b.Property("HpThreshold") + .HasColumnType("smallint"); + + b.Property("IconId") + .HasColumnType("smallint"); + + b.Property("IsHostile") + .HasColumnType("boolean"); + + b.Property("IsPercent") + .HasColumnType("boolean"); + + b.Property("IsPercentileDmg") + .HasColumnType("boolean"); + + b.Property("IsValhallaPartner") + .HasColumnType("boolean"); + + b.Property("JobXp") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("LightResistance") + .HasColumnType("smallint"); + + b.Property("Limiter") + .HasColumnType("smallint"); + + b.Property("MagicDefence") + .HasColumnType("smallint"); + + b.Property("MaxHp") + .HasColumnType("integer"); + + b.Property("MaxMp") + .HasColumnType("integer"); + + b.Property("Midgard") + .HasColumnType("integer"); + + b.Property("MonsterType") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NoAggresiveIcon") + .HasColumnType("boolean"); + + b.Property("NoticeRange") + .HasColumnType("smallint"); + + b.Property("OnDefenseOnlyOnce") + .HasColumnType("boolean"); + + b.Property("PetInfoVal1") + .HasColumnType("integer"); + + b.Property("PetInfoVal2") + .HasColumnType("integer"); + + b.Property("PetInfoVal3") + .HasColumnType("integer"); + + b.Property("PetInfoVal4") + .HasColumnType("integer"); + + b.Property("Race") + .HasColumnType("smallint"); + + b.Property("RaceType") + .HasColumnType("smallint"); + + b.Property("RangeThreshold") + .HasColumnType("smallint"); + + b.Property("RegenerateHpOverTime") + .HasColumnType("boolean"); + + b.Property("RespawnTime") + .HasColumnType("integer"); + + b.Property("SpawnMobOrColor") + .HasColumnType("integer"); + + b.Property("Speed") + .HasColumnType("smallint"); + + b.Property("SpriteSize") + .HasColumnType("smallint"); + + b.Property("TakeDamages") + .HasColumnType("integer"); + + b.Property("VNumRequired") + .HasColumnType("smallint"); + + b.Property("VisibleOnMinimapAsGreenDot") + .HasColumnType("boolean"); + + b.Property("WaterResistance") + .HasColumnType("smallint"); + + b.Property("Xp") + .HasColumnType("integer"); + + b.HasKey("NpcMonsterVNum"); + + b.ToTable("NpcMonster"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.NpcMonsterSkill", b => + { + b.Property("NpcMonsterSkillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NpcMonsterSkillId")); + + b.Property("Force") + .HasColumnType("smallint"); + + b.Property("NpcMonsterVNum") + .HasColumnType("smallint"); + + b.Property("Rate") + .HasColumnType("smallint"); + + b.Property("SkillVNum") + .HasColumnType("smallint"); + + b.HasKey("NpcMonsterSkillId"); + + b.HasIndex("NpcMonsterVNum"); + + b.HasIndex("SkillVNum"); + + b.ToTable("NpcMonsterSkill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.NpcTalk", b => + { + b.Property("DialogId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DialogId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("DialogId"); + + b.ToTable("NpcTalk"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.PenaltyLog", b => + { + b.Property("PenaltyLogId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyLogId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AdminName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("DateStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Penalty") + .HasColumnType("smallint"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("PenaltyLogId"); + + b.HasIndex("AccountId"); + + b.ToTable("PenaltyLog"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Portal", b => + { + b.Property("PortalId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PortalId")); + + b.Property("DestinationMapId") + .HasColumnType("smallint"); + + b.Property("DestinationX") + .HasColumnType("smallint"); + + b.Property("DestinationY") + .HasColumnType("smallint"); + + b.Property("IsDisabled") + .HasColumnType("boolean"); + + b.Property("SourceMapId") + .HasColumnType("smallint"); + + b.Property("SourceX") + .HasColumnType("smallint"); + + b.Property("SourceY") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("PortalId"); + + b.HasIndex("DestinationMapId"); + + b.HasIndex("SourceMapId"); + + b.ToTable("Portal"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Quest", b => + { + b.Property("QuestId") + .HasColumnType("smallint"); + + b.Property("AutoFinish") + .HasColumnType("boolean"); + + b.Property("Desc") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("EndDialogId") + .HasColumnType("integer"); + + b.Property("IsDaily") + .HasColumnType("boolean"); + + b.Property("IsSecondary") + .HasColumnType("boolean"); + + b.Property("LevelMax") + .HasColumnType("smallint"); + + b.Property("LevelMin") + .HasColumnType("smallint"); + + b.Property("NextQuestId") + .HasColumnType("smallint"); + + b.Property("QuestType") + .HasColumnType("smallint"); + + b.Property("RequiredQuestId") + .HasColumnType("smallint"); + + b.Property("SpecialData") + .HasColumnType("integer"); + + b.Property("StartDialogId") + .HasColumnType("integer"); + + b.Property("TargetMap") + .HasColumnType("smallint"); + + b.Property("TargetX") + .HasColumnType("smallint"); + + b.Property("TargetY") + .HasColumnType("smallint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("QuestId"); + + b.ToTable("Quest"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuestObjective", b => + { + b.Property("QuestObjectiveId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FirstData") + .HasColumnType("integer"); + + b.Property("FourthData") + .HasColumnType("integer"); + + b.Property("QuestId") + .HasColumnType("smallint"); + + b.Property("SecondData") + .HasColumnType("integer"); + + b.Property("ThirdData") + .HasColumnType("integer"); + + b.HasKey("QuestObjectiveId"); + + b.HasIndex("QuestId"); + + b.ToTable("QuestObjective"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuestQuestReward", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("QuestId") + .HasColumnType("smallint"); + + b.Property("QuestRewardId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("QuestId"); + + b.HasIndex("QuestRewardId"); + + b.ToTable("QuestQuestReward"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuestReward", b => + { + b.Property("QuestRewardId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("QuestRewardId")); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("Data") + .HasColumnType("integer"); + + b.Property("Design") + .HasColumnType("smallint"); + + b.Property("Rarity") + .HasColumnType("smallint"); + + b.Property("RewardType") + .HasColumnType("smallint"); + + b.Property("Upgrade") + .HasColumnType("smallint"); + + b.HasKey("QuestRewardId"); + + b.ToTable("QuestReward"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuicklistEntry", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("IconType") + .HasColumnType("smallint"); + + b.Property("IconVNum") + .HasColumnType("smallint"); + + b.Property("Morph") + .HasColumnType("smallint"); + + b.Property("QuickListIndex") + .HasColumnType("smallint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("QuicklistEntry"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Recipe", b => + { + b.Property("RecipeId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RecipeId")); + + b.Property("Amount") + .HasColumnType("smallint"); + + b.Property("ItemVNum") + .HasColumnType("smallint"); + + b.Property("MapNpcId") + .HasColumnType("integer"); + + b.HasKey("RecipeId"); + + b.HasIndex("ItemVNum"); + + b.HasIndex("MapNpcId"); + + b.ToTable("Recipe"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.RecipeItem", b => + { + b.Property("RecipeItemId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RecipeItemId")); + + b.Property("Amount") + .HasColumnType("smallint"); + + b.Property("ItemVNum") + .HasColumnType("smallint"); + + b.Property("RecipeId") + .HasColumnType("smallint"); + + b.HasKey("RecipeItemId"); + + b.HasIndex("ItemVNum"); + + b.HasIndex("RecipeId"); + + b.ToTable("RecipeItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Respawn", b => + { + b.Property("RespawnId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RespawnId")); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("RespawnMapTypeId") + .HasColumnType("bigint"); + + b.Property("X") + .HasColumnType("smallint"); + + b.Property("Y") + .HasColumnType("smallint"); + + b.HasKey("RespawnId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("MapId"); + + b.HasIndex("RespawnMapTypeId"); + + b.ToTable("Respawn"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.RespawnMapType", b => + { + b.Property("RespawnMapTypeId") + .HasColumnType("bigint"); + + b.Property("DefaultX") + .HasColumnType("smallint"); + + b.Property("DefaultY") + .HasColumnType("smallint"); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("RespawnMapTypeId"); + + b.HasIndex("MapId"); + + b.ToTable("RespawnMapType"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.RollGeneratedItem", b => + { + b.Property("RollGeneratedItemId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RollGeneratedItemId")); + + b.Property("IsRareRandom") + .HasColumnType("boolean"); + + b.Property("IsSuperReward") + .HasColumnType("boolean"); + + b.Property("ItemGeneratedAmount") + .HasColumnType("smallint"); + + b.Property("ItemGeneratedUpgrade") + .HasColumnType("smallint"); + + b.Property("ItemGeneratedVNum") + .HasColumnType("smallint"); + + b.Property("MaximumOriginalItemRare") + .HasColumnType("smallint"); + + b.Property("MinimumOriginalItemRare") + .HasColumnType("smallint"); + + b.Property("OriginalItemDesign") + .HasColumnType("smallint"); + + b.Property("OriginalItemVNum") + .HasColumnType("smallint"); + + b.Property("Probability") + .HasColumnType("smallint"); + + b.HasKey("RollGeneratedItemId"); + + b.HasIndex("ItemGeneratedVNum"); + + b.HasIndex("OriginalItemVNum"); + + b.ToTable("RollGeneratedItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Script", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Argument1") + .HasColumnType("smallint"); + + b.Property("Argument2") + .HasColumnType("smallint"); + + b.Property("Argument3") + .HasColumnType("smallint"); + + b.Property("ScriptId") + .HasColumnType("smallint"); + + b.Property("ScriptStepId") + .HasColumnType("smallint"); + + b.Property("StepType") + .IsRequired() + .HasColumnType("text"); + + b.Property("StringArgument") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScriptId", "ScriptStepId") + .IsUnique(); + + b.ToTable("Script"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ScriptedInstance", b => + { + b.Property("ScriptedInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ScriptedInstanceId")); + + b.Property("Label") + .HasColumnType("text"); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("PositionX") + .HasColumnType("smallint"); + + b.Property("PositionY") + .HasColumnType("smallint"); + + b.Property("Script") + .HasMaxLength(2147483647) + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("ScriptedInstanceId"); + + b.HasIndex("MapId"); + + b.ToTable("ScriptedInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Shop", b => + { + b.Property("ShopId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ShopId")); + + b.Property("MapNpcId") + .HasColumnType("integer"); + + b.Property("MenuType") + .HasColumnType("smallint"); + + b.Property("ShopType") + .HasColumnType("smallint"); + + b.HasKey("ShopId"); + + b.HasIndex("MapNpcId"); + + b.ToTable("Shop"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ShopItem", b => + { + b.Property("ShopItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ShopItemId")); + + b.Property("Color") + .HasColumnType("smallint"); + + b.Property("ItemVNum") + .HasColumnType("smallint"); + + b.Property("Rare") + .HasColumnType("smallint"); + + b.Property("ShopId") + .HasColumnType("integer"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("Upgrade") + .HasColumnType("smallint"); + + b.HasKey("ShopItemId"); + + b.HasIndex("ItemVNum"); + + b.HasIndex("ShopId"); + + b.ToTable("ShopItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ShopSkill", b => + { + b.Property("ShopSkillId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ShopSkillId")); + + b.Property("ShopId") + .HasColumnType("integer"); + + b.Property("SkillVNum") + .HasColumnType("smallint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("ShopSkillId"); + + b.HasIndex("ShopId"); + + b.HasIndex("SkillVNum"); + + b.ToTable("ShopSkill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Skill", b => + { + b.Property("SkillVNum") + .HasColumnType("smallint"); + + b.Property("AttackAnimation") + .HasColumnType("smallint"); + + b.Property("CastAnimation") + .HasColumnType("smallint"); + + b.Property("CastEffect") + .HasColumnType("smallint"); + + b.Property("CastId") + .HasColumnType("smallint"); + + b.Property("CastTime") + .HasColumnType("smallint"); + + b.Property("Class") + .HasColumnType("smallint"); + + b.Property("Cooldown") + .HasColumnType("smallint"); + + b.Property("CpCost") + .HasColumnType("smallint"); + + b.Property("Duration") + .HasColumnType("smallint"); + + b.Property("Effect") + .HasColumnType("smallint"); + + b.Property("Element") + .HasColumnType("smallint"); + + b.Property("HitType") + .HasColumnType("smallint"); + + b.Property("ItemVNum") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("LevelMinimum") + .HasColumnType("smallint"); + + b.Property("MinimumAdventurerLevel") + .HasColumnType("smallint"); + + b.Property("MinimumArcherLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMagicianLevel") + .HasColumnType("smallint"); + + b.Property("MinimumSwordmanLevel") + .HasColumnType("smallint"); + + b.Property("MpCost") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Price") + .HasColumnType("integer"); + + b.Property("Range") + .HasColumnType("smallint"); + + b.Property("SkillType") + .HasColumnType("smallint"); + + b.Property("TargetRange") + .HasColumnType("smallint"); + + b.Property("TargetType") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UpgradeSkill") + .HasColumnType("smallint"); + + b.Property("UpgradeType") + .HasColumnType("smallint"); + + b.HasKey("SkillVNum"); + + b.ToTable("Skill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.StaticBonus", b => + { + b.Property("StaticBonusId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StaticBonusId")); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("DateEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("StaticBonusType") + .HasColumnType("smallint"); + + b.HasKey("StaticBonusId"); + + b.HasIndex("CharacterId"); + + b.ToTable("StaticBonus"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.StaticBuff", b => + { + b.Property("StaticBuffId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StaticBuffId")); + + b.Property("CardId") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.HasKey("StaticBuffId"); + + b.HasIndex("CardId"); + + b.HasIndex("CharacterId"); + + b.ToTable("StaticBuff"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Teleporter", b => + { + b.Property("TeleporterId") + .ValueGeneratedOnAdd() + .HasColumnType("smallint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TeleporterId")); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("MapId") + .HasColumnType("smallint"); + + b.Property("MapNpcId") + .HasColumnType("integer"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("TeleporterId"); + + b.HasIndex("MapId"); + + b.HasIndex("MapNpcId"); + + b.ToTable("Teleporter"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Title", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("TitleType") + .HasColumnType("smallint"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId", "TitleType") + .IsUnique(); + + b.ToTable("Title"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Warehouse", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("FamilyId"); + + b.ToTable("Warehouse"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.WarehouseItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ItemInstanceId") + .HasColumnType("uuid"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ItemInstanceId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("WarehouseItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.UsableInstance", b => + { + b.HasBaseType("NosCore.Database.Entities.ItemInstance"); + + b.Property("Hp") + .HasColumnType("smallint"); + + b.Property("Mp") + .HasColumnType("smallint"); + + b.ToTable("ItemInstance", t => + { + t.Property("Hp") + .HasColumnName("UsableInstance_Hp"); + + t.Property("Mp") + .HasColumnName("UsableInstance_Mp"); + }); + + b.HasDiscriminator().HasValue("UsableInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.WearableInstance", b => + { + b.HasBaseType("NosCore.Database.Entities.ItemInstance"); + + b.Property("Ammo") + .HasColumnType("smallint"); + + b.Property("Cellon") + .HasColumnType("smallint"); + + b.Property("CloseDefence") + .HasColumnType("smallint"); + + b.Property("Concentrate") + .HasColumnType("smallint"); + + b.Property("CriticalDodge") + .HasColumnType("smallint"); + + b.Property("CriticalLuckRate") + .HasColumnType("smallint"); + + b.Property("CriticalRate") + .HasColumnType("smallint"); + + b.Property("DamageMaximum") + .HasColumnType("smallint"); + + b.Property("DamageMinimum") + .HasColumnType("smallint"); + + b.Property("DarkElement") + .HasColumnType("smallint"); + + b.Property("DarkResistance") + .HasColumnType("smallint"); + + b.Property("DefenceDodge") + .HasColumnType("smallint"); + + b.Property("DistanceDefence") + .HasColumnType("smallint"); + + b.Property("DistanceDefenceDodge") + .HasColumnType("smallint"); + + b.Property("ElementRate") + .HasColumnType("smallint"); + + b.Property("FireElement") + .HasColumnType("smallint"); + + b.Property("FireResistance") + .HasColumnType("smallint"); + + b.Property("HitRate") + .HasColumnType("smallint"); + + b.Property("Hp") + .HasColumnType("smallint"); + + b.Property("IsEmpty") + .HasColumnType("boolean"); + + b.Property("IsFixed") + .HasColumnType("boolean"); + + b.Property("LightElement") + .HasColumnType("smallint"); + + b.Property("LightResistance") + .HasColumnType("smallint"); + + b.Property("MagicDefence") + .HasColumnType("smallint"); + + b.Property("MaxElementRate") + .HasColumnType("smallint"); + + b.Property("Mp") + .HasColumnType("smallint"); + + b.Property("ShellRarity") + .HasColumnType("smallint"); + + b.Property("WaterElement") + .HasColumnType("smallint"); + + b.Property("WaterResistance") + .HasColumnType("smallint"); + + b.Property("Xp") + .HasColumnType("bigint"); + + b.HasDiscriminator().HasValue("WearableInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.SpecialistInstance", b => + { + b.HasBaseType("NosCore.Database.Entities.WearableInstance"); + + b.Property("SlDamage") + .HasColumnType("smallint"); + + b.Property("SlDefence") + .HasColumnType("smallint"); + + b.Property("SlElement") + .HasColumnType("smallint"); + + b.Property("SlHp") + .HasColumnType("smallint"); + + b.Property("SpDamage") + .HasColumnType("smallint"); + + b.Property("SpDark") + .HasColumnType("smallint"); + + b.Property("SpDefence") + .HasColumnType("smallint"); + + b.Property("SpElement") + .HasColumnType("smallint"); + + b.Property("SpFire") + .HasColumnType("smallint"); + + b.Property("SpHp") + .HasColumnType("smallint"); + + b.Property("SpLevel") + .HasColumnType("smallint"); + + b.Property("SpLight") + .HasColumnType("smallint"); + + b.Property("SpStoneUpgrade") + .HasColumnType("smallint"); + + b.Property("SpWater") + .HasColumnType("smallint"); + + b.HasDiscriminator().HasValue("SpecialistInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.BoxInstance", b => + { + b.HasBaseType("NosCore.Database.Entities.SpecialistInstance"); + + b.Property("HoldingVNum") + .HasColumnType("smallint"); + + b.HasDiscriminator().HasValue("BoxInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ActPart", b => + { + b.HasOne("NosCore.Database.Entities.Act", "Act") + .WithMany("ActParts") + .HasForeignKey("ActId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Act"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.BCard", b => + { + b.HasOne("NosCore.Database.Entities.Card", "Card") + .WithMany("BCards") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.Item", "Item") + .WithMany("BCards") + .HasForeignKey("ItemVNum") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.NpcMonster", "NpcMonster") + .WithMany("BCards") + .HasForeignKey("NpcMonsterVNum") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.Skill", "Skill") + .WithMany("BCards") + .HasForeignKey("SkillVNum") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Card"); + + b.Navigation("Item"); + + b.Navigation("NpcMonster"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.BazaarItem", b => + { + b.HasOne("NosCore.Database.Entities.ItemInstance", "ItemInstance") + .WithMany("BazaarItem") + .HasForeignKey("ItemInstanceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Character", "Seller") + .WithMany("BazaarItem") + .HasForeignKey("SellerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ItemInstance"); + + b.Navigation("Seller"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Character", b => + { + b.HasOne("NosCore.Database.Entities.Account", "Account") + .WithMany("Character") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Script", "Script") + .WithMany("Characters") + .HasForeignKey("CurrentScriptId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("Character") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Map"); + + b.Navigation("Script"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterActPart", b => + { + b.HasOne("NosCore.Database.Entities.ActPart", "ActPart") + .WithMany("CharacterActParts") + .HasForeignKey("ActPartId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("CharacterActParts") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ActPart"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterQuest", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("CharacterQuest") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Quest", "Quest") + .WithMany("CharacterQuest") + .HasForeignKey("QuestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("Quest"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterQuestObjective", b => + { + b.HasOne("NosCore.Database.Entities.CharacterQuest", "CharacterQuest") + .WithMany("CharacterQuestObjective") + .HasForeignKey("CharacterQuestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.QuestObjective", "QuestObjective") + .WithMany("CharacterQuestObjective") + .HasForeignKey("QuestObjectiveId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CharacterQuest"); + + b.Navigation("QuestObjective"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterRelation", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character1") + .WithMany("CharacterRelation1") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Character", "Character2") + .WithMany("CharacterRelation2") + .HasForeignKey("RelatedCharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character1"); + + b.Navigation("Character2"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterSkill", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("CharacterSkill") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Skill", "Skill") + .WithMany("CharacterSkill") + .HasForeignKey("SkillVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Combo", b => + { + b.HasOne("NosCore.Database.Entities.Skill", "Skill") + .WithMany("Combo") + .HasForeignKey("SkillVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Drop", b => + { + b.HasOne("NosCore.Database.Entities.MapType", "MapType") + .WithMany("Drops") + .HasForeignKey("MapTypeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.NpcMonster", "NpcMonster") + .WithMany("Drop") + .HasForeignKey("MonsterVNum") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.Item", "Item") + .WithMany("Drop") + .HasForeignKey("VNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("MapType"); + + b.Navigation("NpcMonster"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.EquipmentOption", b => + { + b.HasOne("NosCore.Database.Entities.WearableInstance", "WearableInstance") + .WithMany() + .HasForeignKey("WearableInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WearableInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.FamilyCharacter", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("FamilyCharacter") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Family", "Family") + .WithMany("FamilyCharacters") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.FamilyLog", b => + { + b.HasOne("NosCore.Database.Entities.Family", "Family") + .WithMany("FamilyLogs") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.InventoryItemInstance", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("Inventory") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.ItemInstance", "ItemInstance") + .WithMany("InventoryItemInstance") + .HasForeignKey("ItemInstanceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("ItemInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ItemInstance", b => + { + b.HasOne("NosCore.Database.Entities.Character", "BoundCharacter") + .WithMany() + .HasForeignKey("BoundCharacterId"); + + b.HasOne("NosCore.Database.Entities.Item", "Item") + .WithMany("ItemInstances") + .HasForeignKey("ItemVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BoundCharacter"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Mail", b => + { + b.HasOne("NosCore.Database.Entities.ItemInstance", "ItemInstance") + .WithMany("Mail") + .HasForeignKey("ItemInstanceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.Character", "Receiver") + .WithMany("Mail1") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Character", "Sender") + .WithMany("Mail") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ItemInstance"); + + b.Navigation("Receiver"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapMonster", b => + { + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("MapMonster") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.NpcMonster", "NpcMonster") + .WithMany("MapMonster") + .HasForeignKey("VNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("NpcMonster"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapNpc", b => + { + b.HasOne("NosCore.Database.Entities.NpcTalk", "NpcTalk") + .WithMany("MapNpc") + .HasForeignKey("Dialog") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("MapNpc") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.NpcMonster", "NpcMonster") + .WithMany("MapNpc") + .HasForeignKey("VNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("NpcMonster"); + + b.Navigation("NpcTalk"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapType", b => + { + b.HasOne("NosCore.Database.Entities.RespawnMapType", "RespawnMapType") + .WithMany("MapTypes") + .HasForeignKey("RespawnMapTypeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.RespawnMapType", "ReturnMapType") + .WithMany("MapTypes1") + .HasForeignKey("ReturnMapTypeId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("RespawnMapType"); + + b.Navigation("ReturnMapType"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapTypeMap", b => + { + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("MapTypeMap") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.MapType", "MapType") + .WithMany("MapTypeMap") + .HasForeignKey("MapTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("MapType"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Mate", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("Mate") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.NpcMonster", "NpcMonster") + .WithMany("Mate") + .HasForeignKey("VNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("NpcMonster"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Miniland", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Owner") + .WithMany("Miniland") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MinilandObject", b => + { + b.HasOne("NosCore.Database.Entities.InventoryItemInstance", "InventoryItemInstance") + .WithMany("MinilandObject") + .HasForeignKey("InventoryItemInstanceId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("InventoryItemInstance"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.NpcMonsterSkill", b => + { + b.HasOne("NosCore.Database.Entities.NpcMonster", "NpcMonster") + .WithMany("NpcMonsterSkill") + .HasForeignKey("NpcMonsterVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Skill", "Skill") + .WithMany("NpcMonsterSkill") + .HasForeignKey("SkillVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("NpcMonster"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.PenaltyLog", b => + { + b.HasOne("NosCore.Database.Entities.Account", "Account") + .WithMany("PenaltyLog") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Portal", b => + { + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("Portal") + .HasForeignKey("DestinationMapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Map", "Map1") + .WithMany("Portal1") + .HasForeignKey("SourceMapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Map1"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuestObjective", b => + { + b.HasOne("NosCore.Database.Entities.Quest", "Quest") + .WithMany("QuestObjective") + .HasForeignKey("QuestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Quest"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuestQuestReward", b => + { + b.HasOne("NosCore.Database.Entities.Quest", "Quest") + .WithMany("QuestQuestReward") + .HasForeignKey("QuestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.QuestReward", "QuestReward") + .WithMany("QuestQuestReward") + .HasForeignKey("QuestRewardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Quest"); + + b.Navigation("QuestReward"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuicklistEntry", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("QuicklistEntry") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Recipe", b => + { + b.HasOne("NosCore.Database.Entities.Item", "Item") + .WithMany("Recipe") + .HasForeignKey("ItemVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.MapNpc", "MapNpc") + .WithMany("Recipe") + .HasForeignKey("MapNpcId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("MapNpc"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.RecipeItem", b => + { + b.HasOne("NosCore.Database.Entities.Item", "Item") + .WithMany("RecipeItem") + .HasForeignKey("ItemVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Recipe", "Recipe") + .WithMany("RecipeItem") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Respawn", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("Respawn") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("Respawn") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.RespawnMapType", "RespawnMapType") + .WithMany("Respawn") + .HasForeignKey("RespawnMapTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("Map"); + + b.Navigation("RespawnMapType"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.RespawnMapType", b => + { + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("RespawnMapType") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.RollGeneratedItem", b => + { + b.HasOne("NosCore.Database.Entities.Item", "ItemGenerated") + .WithMany("RollGeneratedItem2") + .HasForeignKey("ItemGeneratedVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Item", "OriginalItem") + .WithMany("RollGeneratedItem") + .HasForeignKey("OriginalItemVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ItemGenerated"); + + b.Navigation("OriginalItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ScriptedInstance", b => + { + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("ScriptedInstance") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Shop", b => + { + b.HasOne("NosCore.Database.Entities.MapNpc", "MapNpc") + .WithMany("Shop") + .HasForeignKey("MapNpcId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MapNpc"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ShopItem", b => + { + b.HasOne("NosCore.Database.Entities.Item", "Item") + .WithMany("ShopItem") + .HasForeignKey("ItemVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Shop", "Shop") + .WithMany("ShopItem") + .HasForeignKey("ShopId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Shop"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ShopSkill", b => + { + b.HasOne("NosCore.Database.Entities.Shop", "Shop") + .WithMany("ShopSkill") + .HasForeignKey("ShopId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Skill", "Skill") + .WithMany("ShopSkill") + .HasForeignKey("SkillVNum") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Shop"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.StaticBonus", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("StaticBonus") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.StaticBuff", b => + { + b.HasOne("NosCore.Database.Entities.Card", "Card") + .WithMany("StaticBuff") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("StaticBuff") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Card"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Teleporter", b => + { + b.HasOne("NosCore.Database.Entities.Map", "Map") + .WithMany("Teleporter") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.MapNpc", "MapNpc") + .WithMany("Teleporter") + .HasForeignKey("MapNpcId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("MapNpc"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Title", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("Title") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Warehouse", b => + { + b.HasOne("NosCore.Database.Entities.Character", "Character") + .WithMany("Warehouses") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("NosCore.Database.Entities.Family", "Family") + .WithMany("Warehouses") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Character"); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.WarehouseItem", b => + { + b.HasOne("NosCore.Database.Entities.ItemInstance", "ItemInstance") + .WithMany("WarehouseItems") + .HasForeignKey("ItemInstanceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("NosCore.Database.Entities.Warehouse", "Warehouse") + .WithMany("WarehouseItems") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ItemInstance"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Account", b => + { + b.Navigation("Character"); + + b.Navigation("PenaltyLog"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Act", b => + { + b.Navigation("ActParts"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ActPart", b => + { + b.Navigation("CharacterActParts"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Card", b => + { + b.Navigation("BCards"); + + b.Navigation("StaticBuff"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Character", b => + { + b.Navigation("BazaarItem"); + + b.Navigation("CharacterActParts"); + + b.Navigation("CharacterQuest"); + + b.Navigation("CharacterRelation1"); + + b.Navigation("CharacterRelation2"); + + b.Navigation("CharacterSkill"); + + b.Navigation("FamilyCharacter"); + + b.Navigation("Inventory"); + + b.Navigation("Mail"); + + b.Navigation("Mail1"); + + b.Navigation("Mate"); + + b.Navigation("Miniland"); + + b.Navigation("QuicklistEntry"); + + b.Navigation("Respawn"); + + b.Navigation("StaticBonus"); + + b.Navigation("StaticBuff"); + + b.Navigation("Title"); + + b.Navigation("Warehouses"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.CharacterQuest", b => + { + b.Navigation("CharacterQuestObjective"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Family", b => + { + b.Navigation("FamilyCharacters"); + + b.Navigation("FamilyLogs"); + + b.Navigation("Warehouses"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.InventoryItemInstance", b => + { + b.Navigation("MinilandObject"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Item", b => + { + b.Navigation("BCards"); + + b.Navigation("Drop"); + + b.Navigation("ItemInstances"); + + b.Navigation("Recipe"); + + b.Navigation("RecipeItem"); + + b.Navigation("RollGeneratedItem"); + + b.Navigation("RollGeneratedItem2"); + + b.Navigation("ShopItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.ItemInstance", b => + { + b.Navigation("BazaarItem"); + + b.Navigation("InventoryItemInstance"); + + b.Navigation("Mail"); + + b.Navigation("WarehouseItems"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Map", b => + { + b.Navigation("Character"); + + b.Navigation("MapMonster"); + + b.Navigation("MapNpc"); + + b.Navigation("MapTypeMap"); + + b.Navigation("Portal"); + + b.Navigation("Portal1"); + + b.Navigation("Respawn"); + + b.Navigation("RespawnMapType"); + + b.Navigation("ScriptedInstance"); + + b.Navigation("Teleporter"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapNpc", b => + { + b.Navigation("Recipe"); + + b.Navigation("Shop"); + + b.Navigation("Teleporter"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.MapType", b => + { + b.Navigation("Drops"); + + b.Navigation("MapTypeMap"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.NpcMonster", b => + { + b.Navigation("BCards"); + + b.Navigation("Drop"); + + b.Navigation("MapMonster"); + + b.Navigation("MapNpc"); + + b.Navigation("Mate"); + + b.Navigation("NpcMonsterSkill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.NpcTalk", b => + { + b.Navigation("MapNpc"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Quest", b => + { + b.Navigation("CharacterQuest"); + + b.Navigation("QuestObjective"); + + b.Navigation("QuestQuestReward"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuestObjective", b => + { + b.Navigation("CharacterQuestObjective"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.QuestReward", b => + { + b.Navigation("QuestQuestReward"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Recipe", b => + { + b.Navigation("RecipeItem"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.RespawnMapType", b => + { + b.Navigation("MapTypes"); + + b.Navigation("MapTypes1"); + + b.Navigation("Respawn"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Script", b => + { + b.Navigation("Characters"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Shop", b => + { + b.Navigation("ShopItem"); + + b.Navigation("ShopSkill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Skill", b => + { + b.Navigation("BCards"); + + b.Navigation("CharacterSkill"); + + b.Navigation("Combo"); + + b.Navigation("NpcMonsterSkill"); + + b.Navigation("ShopSkill"); + }); + + modelBuilder.Entity("NosCore.Database.Entities.Warehouse", b => + { + b.Navigation("WarehouseItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.cs b/src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.cs new file mode 100644 index 000000000..a05ba4e81 --- /dev/null +++ b/src/NosCore.Database/Migrations/20260424144528_AddMateCaptureItemEffect.cs @@ -0,0 +1,134 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NosCore.Database.Migrations +{ + /// + public partial class AddMateCaptureItemEffect : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:audit_log_type", "account_creation,character_creation,email_update") + .Annotation("Npgsql:Enum:authority_type", "user,moderator,game_master,administrator,root,closed,banned,unconfirmed") + .Annotation("Npgsql:Enum:character_class_type", "adventurer,swordsman,archer,mage,martial_artist") + .Annotation("Npgsql:Enum:character_relation_type", "friend,hidden_spouse,spouse,blocked") + .Annotation("Npgsql:Enum:character_state", "active,inactive") + .Annotation("Npgsql:Enum:element_type", "neutral,fire,water,light,dark") + .Annotation("Npgsql:Enum:equipment_type", "main_weapon,armor,hat,gloves,boots,secondary_weapon,necklace,ring,bracelet,mask,fairy,amulet,sp,costume_suit,costume_hat,weapon_skin,wing_skin") + .Annotation("Npgsql:Enum:family_authority", "head,assistant,manager,member") + .Annotation("Npgsql:Enum:family_authority_type", "none,put,all") + .Annotation("Npgsql:Enum:family_log_type", "daily_message,raid_won,rainbow_battle,family_xp,family_level_up,level_up,item_upgraded,right_changed,authority_changed,family_managed,user_managed,ware_house_added,ware_house_removed") + .Annotation("Npgsql:Enum:family_member_rank", "nothing,old_uncle,old_aunt,father,mother,uncle,aunt,brother,sister,spouse,brother2,sister2,old_son,old_daugter,middle_son,middle_daughter,young_son,young_daugter,old_little_son,old_little_daughter,little_son,little_daughter,middle_little_son,middle_little_daugter") + .Annotation("Npgsql:Enum:gender_type", "male,female") + .Annotation("Npgsql:Enum:hair_color_type", "dark_purple,yellow,blue,purple,orange,brown,green,dark_grey,light_blue,pink_red,light_yellow,light_pink,light_green,light_grey,sky_blue,black,dark_orange,dark_orange_variant2,dark_orange_variant3,dark_orange_variant4,dark_orange_variant5,dark_orange_variant6,light_orange,light_light_orange,light_light_light_orange,light_light_light_light_orange,super_light_orange,dark_yellow,light_light_yellow,kaki_yellow,super_light_yellow,super_light_yellow2,super_light_yellow3,little_dark_yellow,yellow_variant,yellow_variant1,yellow_variant2,yellow_variant3,yellow_variant4,yellow_variant5,yellow_variant6,yellow_variant7,yellow_variant8,yellow_variant9,green_variant,green_variant1,dark_green_variant,green_more_dark_variant,green_variant2,green_variant3,green_variant4,green_variant5,green_variant6,green_variant7,green_variant8,green_variant9,green_variant10,green_variant11,green_variant12,green_variant13,green_variant14,green_variant15,green_variant16,green_variant17,green_variant18,green_variant19,green_variant20,light_blue_variant1,light_blue_variant2,light_blue_variant3,light_blue_variant4,light_blue_variant5,light_blue_variant6,light_blue_variant7,light_blue_variant8,light_blue_variant9,light_blue_variant10,light_blue_variant11,light_blue_variant12,light_blue_variant13,dark_black,light_blue_variant14,light_blue_variant15,light_blue_variant16,light_blue_variant17,blue_variant,blue_variant_dark,blue_variant_dark_dark,blue_variant_dark_dark2,flash_blue,flash_blue_dark,flash_blue_dark2,flash_blue_dark3,flash_blue_dark4,flash_blue_dark5,flash_blue_dark6,flash_blue_dark7,flash_blue_dark8,flash_blue_dark9,white,flash_blue_dark10,flash_blue1,flash_blue2,flash_blue3,flash_blue4,flash_blue5,flash_purple,flash_light_purple,flash_light_purple2,flash_light_purple3,flash_light_purple4,flash_light_purple5,light_purple,purple_variant1,purple_variant2,purple_variant3,purple_variant4,purple_variant5,purple_variant6,purple_variant7,purple_variant8,purple_variant9,purple_variant10,purple_variant11,purple_variant12,purple_variant13,purple_variant14,purple_variant15") + .Annotation("Npgsql:Enum:hair_style_type", "hair_style_a,hair_style_b,hair_style_c,hair_style_d,no_hair") + .Annotation("Npgsql:Enum:item_effect_type", "no_effect,teleport,apply_hair_die,apply_hair_style,speaker,marriage_proposal,undefined,sp_charger,mate_capture,dropped_sp_recharger,premium_sp_recharger,crafted_sp_recharger,specialist_medal,apply_skin_partner,change_gender,point_initialisation,sealed_tarot_card,tarot_card,red_amulet,blue_amulet,reinforcement_amulet,heroic,random_heroic,attack_amulet,defense_amulet,speed_booster,box_effect,vehicle,gold_nos_merchant_upgrade,silver_nos_merchant_upgrade,inventory_upgrade,pet_space_upgrade,pet_basket_upgrade,pet_backpack_upgrade,inventory_ticket_upgrade,buff_potions,marriage_separation") + .Annotation("Npgsql:Enum:item_type", "weapon,armor,fashion,jewelery,specialist,box,shell,main,upgrade,production,map,special,potion,event,title,quest1,sell,food,snack,magical,part,teacher,ammo,quest2,house,garden,minigame,terrace,miniland_theme") + .Annotation("Npgsql:Enum:mate_type", "partner,pet") + .Annotation("Npgsql:Enum:miniland_state", "open,private,lock") + .Annotation("Npgsql:Enum:monster_type", "unknown,partner,npc,well,portal,boss,elite,peapod,special,gem_space_time") + .Annotation("Npgsql:Enum:noscore_pocket_type", "equipment,main,etc,miniland,specialist,costume,wear,mount,raid") + .Annotation("Npgsql:Enum:penalty_type", "muted,banned,block_exp,block_f_exp,block_rep,warning") + .Annotation("Npgsql:Enum:portal_type", "ts_normal,closed,open,miniland,ts_end,ts_end_closed,exit,exit_closed,raid,effect,blue_raid,dark_raid,time_space,shop_teleport,map_portal") + .Annotation("Npgsql:Enum:quest_type", "hunt,special_collect,collect_in_raid,brings,capture_without_getting_the_monster,capture,times_space,product,number_of_kill,target_reput,ts_point,dialog1,collect_in_ts,required,wear,needed,collect,transmit_gold,go_to,collect_map_entity,use,dialog2,un_know,inspect,win_raid,flower_quest") + .Annotation("Npgsql:Enum:region_type", "en,de,fr,it,pl,es,ru,cs,tr") + .Annotation("Npgsql:Enum:scripted_instance_type", "time_space,raid,raid_act4") + .Annotation("Npgsql:Enum:static_bonus_type", "bazaar_medal_gold,bazaar_medal_silver,back_pack,pet_basket,pet_back_pack,inventory_ticket_upgrade") + .Annotation("Npgsql:Enum:teleporter_type", "teleporter,teleporter_on_map") + .Annotation("Npgsql:Enum:warehouse_type", "warehouse,family_ware_house,pet_warehouse") + .OldAnnotation("Npgsql:Enum:audit_log_type", "account_creation,character_creation,email_update") + .OldAnnotation("Npgsql:Enum:authority_type", "user,moderator,game_master,administrator,root,closed,banned,unconfirmed") + .OldAnnotation("Npgsql:Enum:character_class_type", "adventurer,swordsman,archer,mage,martial_artist") + .OldAnnotation("Npgsql:Enum:character_relation_type", "friend,hidden_spouse,spouse,blocked") + .OldAnnotation("Npgsql:Enum:character_state", "active,inactive") + .OldAnnotation("Npgsql:Enum:element_type", "neutral,fire,water,light,dark") + .OldAnnotation("Npgsql:Enum:equipment_type", "main_weapon,armor,hat,gloves,boots,secondary_weapon,necklace,ring,bracelet,mask,fairy,amulet,sp,costume_suit,costume_hat,weapon_skin,wing_skin") + .OldAnnotation("Npgsql:Enum:family_authority", "head,assistant,manager,member") + .OldAnnotation("Npgsql:Enum:family_authority_type", "none,put,all") + .OldAnnotation("Npgsql:Enum:family_log_type", "daily_message,raid_won,rainbow_battle,family_xp,family_level_up,level_up,item_upgraded,right_changed,authority_changed,family_managed,user_managed,ware_house_added,ware_house_removed") + .OldAnnotation("Npgsql:Enum:family_member_rank", "nothing,old_uncle,old_aunt,father,mother,uncle,aunt,brother,sister,spouse,brother2,sister2,old_son,old_daugter,middle_son,middle_daughter,young_son,young_daugter,old_little_son,old_little_daughter,little_son,little_daughter,middle_little_son,middle_little_daugter") + .OldAnnotation("Npgsql:Enum:gender_type", "male,female") + .OldAnnotation("Npgsql:Enum:hair_color_type", "dark_purple,yellow,blue,purple,orange,brown,green,dark_grey,light_blue,pink_red,light_yellow,light_pink,light_green,light_grey,sky_blue,black,dark_orange,dark_orange_variant2,dark_orange_variant3,dark_orange_variant4,dark_orange_variant5,dark_orange_variant6,light_orange,light_light_orange,light_light_light_orange,light_light_light_light_orange,super_light_orange,dark_yellow,light_light_yellow,kaki_yellow,super_light_yellow,super_light_yellow2,super_light_yellow3,little_dark_yellow,yellow_variant,yellow_variant1,yellow_variant2,yellow_variant3,yellow_variant4,yellow_variant5,yellow_variant6,yellow_variant7,yellow_variant8,yellow_variant9,green_variant,green_variant1,dark_green_variant,green_more_dark_variant,green_variant2,green_variant3,green_variant4,green_variant5,green_variant6,green_variant7,green_variant8,green_variant9,green_variant10,green_variant11,green_variant12,green_variant13,green_variant14,green_variant15,green_variant16,green_variant17,green_variant18,green_variant19,green_variant20,light_blue_variant1,light_blue_variant2,light_blue_variant3,light_blue_variant4,light_blue_variant5,light_blue_variant6,light_blue_variant7,light_blue_variant8,light_blue_variant9,light_blue_variant10,light_blue_variant11,light_blue_variant12,light_blue_variant13,dark_black,light_blue_variant14,light_blue_variant15,light_blue_variant16,light_blue_variant17,blue_variant,blue_variant_dark,blue_variant_dark_dark,blue_variant_dark_dark2,flash_blue,flash_blue_dark,flash_blue_dark2,flash_blue_dark3,flash_blue_dark4,flash_blue_dark5,flash_blue_dark6,flash_blue_dark7,flash_blue_dark8,flash_blue_dark9,white,flash_blue_dark10,flash_blue1,flash_blue2,flash_blue3,flash_blue4,flash_blue5,flash_purple,flash_light_purple,flash_light_purple2,flash_light_purple3,flash_light_purple4,flash_light_purple5,light_purple,purple_variant1,purple_variant2,purple_variant3,purple_variant4,purple_variant5,purple_variant6,purple_variant7,purple_variant8,purple_variant9,purple_variant10,purple_variant11,purple_variant12,purple_variant13,purple_variant14,purple_variant15") + .OldAnnotation("Npgsql:Enum:hair_style_type", "hair_style_a,hair_style_b,hair_style_c,hair_style_d,no_hair") + .OldAnnotation("Npgsql:Enum:item_effect_type", "no_effect,teleport,apply_hair_die,apply_hair_style,speaker,marriage_proposal,undefined,sp_charger,dropped_sp_recharger,premium_sp_recharger,crafted_sp_recharger,specialist_medal,apply_skin_partner,change_gender,point_initialisation,sealed_tarot_card,tarot_card,red_amulet,blue_amulet,reinforcement_amulet,heroic,random_heroic,attack_amulet,defense_amulet,speed_booster,box_effect,vehicle,gold_nos_merchant_upgrade,silver_nos_merchant_upgrade,inventory_upgrade,pet_space_upgrade,pet_basket_upgrade,pet_backpack_upgrade,inventory_ticket_upgrade,buff_potions,marriage_separation") + .OldAnnotation("Npgsql:Enum:item_type", "weapon,armor,fashion,jewelery,specialist,box,shell,main,upgrade,production,map,special,potion,event,title,quest1,sell,food,snack,magical,part,teacher,ammo,quest2,house,garden,minigame,terrace,miniland_theme") + .OldAnnotation("Npgsql:Enum:mate_type", "partner,pet") + .OldAnnotation("Npgsql:Enum:miniland_state", "open,private,lock") + .OldAnnotation("Npgsql:Enum:monster_type", "unknown,partner,npc,well,portal,boss,elite,peapod,special,gem_space_time") + .OldAnnotation("Npgsql:Enum:noscore_pocket_type", "equipment,main,etc,miniland,specialist,costume,wear,mount,raid") + .OldAnnotation("Npgsql:Enum:penalty_type", "muted,banned,block_exp,block_f_exp,block_rep,warning") + .OldAnnotation("Npgsql:Enum:portal_type", "ts_normal,closed,open,miniland,ts_end,ts_end_closed,exit,exit_closed,raid,effect,blue_raid,dark_raid,time_space,shop_teleport,map_portal") + .OldAnnotation("Npgsql:Enum:quest_type", "hunt,special_collect,collect_in_raid,brings,capture_without_getting_the_monster,capture,times_space,product,number_of_kill,target_reput,ts_point,dialog1,collect_in_ts,required,wear,needed,collect,transmit_gold,go_to,collect_map_entity,use,dialog2,un_know,inspect,win_raid,flower_quest") + .OldAnnotation("Npgsql:Enum:region_type", "en,de,fr,it,pl,es,ru,cs,tr") + .OldAnnotation("Npgsql:Enum:scripted_instance_type", "time_space,raid,raid_act4") + .OldAnnotation("Npgsql:Enum:static_bonus_type", "bazaar_medal_gold,bazaar_medal_silver,back_pack,pet_basket,pet_back_pack,inventory_ticket_upgrade") + .OldAnnotation("Npgsql:Enum:teleporter_type", "teleporter,teleporter_on_map") + .OldAnnotation("Npgsql:Enum:warehouse_type", "warehouse,family_ware_house,pet_warehouse"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:audit_log_type", "account_creation,character_creation,email_update") + .Annotation("Npgsql:Enum:authority_type", "user,moderator,game_master,administrator,root,closed,banned,unconfirmed") + .Annotation("Npgsql:Enum:character_class_type", "adventurer,swordsman,archer,mage,martial_artist") + .Annotation("Npgsql:Enum:character_relation_type", "friend,hidden_spouse,spouse,blocked") + .Annotation("Npgsql:Enum:character_state", "active,inactive") + .Annotation("Npgsql:Enum:element_type", "neutral,fire,water,light,dark") + .Annotation("Npgsql:Enum:equipment_type", "main_weapon,armor,hat,gloves,boots,secondary_weapon,necklace,ring,bracelet,mask,fairy,amulet,sp,costume_suit,costume_hat,weapon_skin,wing_skin") + .Annotation("Npgsql:Enum:family_authority", "head,assistant,manager,member") + .Annotation("Npgsql:Enum:family_authority_type", "none,put,all") + .Annotation("Npgsql:Enum:family_log_type", "daily_message,raid_won,rainbow_battle,family_xp,family_level_up,level_up,item_upgraded,right_changed,authority_changed,family_managed,user_managed,ware_house_added,ware_house_removed") + .Annotation("Npgsql:Enum:family_member_rank", "nothing,old_uncle,old_aunt,father,mother,uncle,aunt,brother,sister,spouse,brother2,sister2,old_son,old_daugter,middle_son,middle_daughter,young_son,young_daugter,old_little_son,old_little_daughter,little_son,little_daughter,middle_little_son,middle_little_daugter") + .Annotation("Npgsql:Enum:gender_type", "male,female") + .Annotation("Npgsql:Enum:hair_color_type", "dark_purple,yellow,blue,purple,orange,brown,green,dark_grey,light_blue,pink_red,light_yellow,light_pink,light_green,light_grey,sky_blue,black,dark_orange,dark_orange_variant2,dark_orange_variant3,dark_orange_variant4,dark_orange_variant5,dark_orange_variant6,light_orange,light_light_orange,light_light_light_orange,light_light_light_light_orange,super_light_orange,dark_yellow,light_light_yellow,kaki_yellow,super_light_yellow,super_light_yellow2,super_light_yellow3,little_dark_yellow,yellow_variant,yellow_variant1,yellow_variant2,yellow_variant3,yellow_variant4,yellow_variant5,yellow_variant6,yellow_variant7,yellow_variant8,yellow_variant9,green_variant,green_variant1,dark_green_variant,green_more_dark_variant,green_variant2,green_variant3,green_variant4,green_variant5,green_variant6,green_variant7,green_variant8,green_variant9,green_variant10,green_variant11,green_variant12,green_variant13,green_variant14,green_variant15,green_variant16,green_variant17,green_variant18,green_variant19,green_variant20,light_blue_variant1,light_blue_variant2,light_blue_variant3,light_blue_variant4,light_blue_variant5,light_blue_variant6,light_blue_variant7,light_blue_variant8,light_blue_variant9,light_blue_variant10,light_blue_variant11,light_blue_variant12,light_blue_variant13,dark_black,light_blue_variant14,light_blue_variant15,light_blue_variant16,light_blue_variant17,blue_variant,blue_variant_dark,blue_variant_dark_dark,blue_variant_dark_dark2,flash_blue,flash_blue_dark,flash_blue_dark2,flash_blue_dark3,flash_blue_dark4,flash_blue_dark5,flash_blue_dark6,flash_blue_dark7,flash_blue_dark8,flash_blue_dark9,white,flash_blue_dark10,flash_blue1,flash_blue2,flash_blue3,flash_blue4,flash_blue5,flash_purple,flash_light_purple,flash_light_purple2,flash_light_purple3,flash_light_purple4,flash_light_purple5,light_purple,purple_variant1,purple_variant2,purple_variant3,purple_variant4,purple_variant5,purple_variant6,purple_variant7,purple_variant8,purple_variant9,purple_variant10,purple_variant11,purple_variant12,purple_variant13,purple_variant14,purple_variant15") + .Annotation("Npgsql:Enum:hair_style_type", "hair_style_a,hair_style_b,hair_style_c,hair_style_d,no_hair") + .Annotation("Npgsql:Enum:item_effect_type", "no_effect,teleport,apply_hair_die,apply_hair_style,speaker,marriage_proposal,undefined,sp_charger,dropped_sp_recharger,premium_sp_recharger,crafted_sp_recharger,specialist_medal,apply_skin_partner,change_gender,point_initialisation,sealed_tarot_card,tarot_card,red_amulet,blue_amulet,reinforcement_amulet,heroic,random_heroic,attack_amulet,defense_amulet,speed_booster,box_effect,vehicle,gold_nos_merchant_upgrade,silver_nos_merchant_upgrade,inventory_upgrade,pet_space_upgrade,pet_basket_upgrade,pet_backpack_upgrade,inventory_ticket_upgrade,buff_potions,marriage_separation") + .Annotation("Npgsql:Enum:item_type", "weapon,armor,fashion,jewelery,specialist,box,shell,main,upgrade,production,map,special,potion,event,title,quest1,sell,food,snack,magical,part,teacher,ammo,quest2,house,garden,minigame,terrace,miniland_theme") + .Annotation("Npgsql:Enum:mate_type", "partner,pet") + .Annotation("Npgsql:Enum:miniland_state", "open,private,lock") + .Annotation("Npgsql:Enum:monster_type", "unknown,partner,npc,well,portal,boss,elite,peapod,special,gem_space_time") + .Annotation("Npgsql:Enum:noscore_pocket_type", "equipment,main,etc,miniland,specialist,costume,wear,mount,raid") + .Annotation("Npgsql:Enum:penalty_type", "muted,banned,block_exp,block_f_exp,block_rep,warning") + .Annotation("Npgsql:Enum:portal_type", "ts_normal,closed,open,miniland,ts_end,ts_end_closed,exit,exit_closed,raid,effect,blue_raid,dark_raid,time_space,shop_teleport,map_portal") + .Annotation("Npgsql:Enum:quest_type", "hunt,special_collect,collect_in_raid,brings,capture_without_getting_the_monster,capture,times_space,product,number_of_kill,target_reput,ts_point,dialog1,collect_in_ts,required,wear,needed,collect,transmit_gold,go_to,collect_map_entity,use,dialog2,un_know,inspect,win_raid,flower_quest") + .Annotation("Npgsql:Enum:region_type", "en,de,fr,it,pl,es,ru,cs,tr") + .Annotation("Npgsql:Enum:scripted_instance_type", "time_space,raid,raid_act4") + .Annotation("Npgsql:Enum:static_bonus_type", "bazaar_medal_gold,bazaar_medal_silver,back_pack,pet_basket,pet_back_pack,inventory_ticket_upgrade") + .Annotation("Npgsql:Enum:teleporter_type", "teleporter,teleporter_on_map") + .Annotation("Npgsql:Enum:warehouse_type", "warehouse,family_ware_house,pet_warehouse") + .OldAnnotation("Npgsql:Enum:audit_log_type", "account_creation,character_creation,email_update") + .OldAnnotation("Npgsql:Enum:authority_type", "user,moderator,game_master,administrator,root,closed,banned,unconfirmed") + .OldAnnotation("Npgsql:Enum:character_class_type", "adventurer,swordsman,archer,mage,martial_artist") + .OldAnnotation("Npgsql:Enum:character_relation_type", "friend,hidden_spouse,spouse,blocked") + .OldAnnotation("Npgsql:Enum:character_state", "active,inactive") + .OldAnnotation("Npgsql:Enum:element_type", "neutral,fire,water,light,dark") + .OldAnnotation("Npgsql:Enum:equipment_type", "main_weapon,armor,hat,gloves,boots,secondary_weapon,necklace,ring,bracelet,mask,fairy,amulet,sp,costume_suit,costume_hat,weapon_skin,wing_skin") + .OldAnnotation("Npgsql:Enum:family_authority", "head,assistant,manager,member") + .OldAnnotation("Npgsql:Enum:family_authority_type", "none,put,all") + .OldAnnotation("Npgsql:Enum:family_log_type", "daily_message,raid_won,rainbow_battle,family_xp,family_level_up,level_up,item_upgraded,right_changed,authority_changed,family_managed,user_managed,ware_house_added,ware_house_removed") + .OldAnnotation("Npgsql:Enum:family_member_rank", "nothing,old_uncle,old_aunt,father,mother,uncle,aunt,brother,sister,spouse,brother2,sister2,old_son,old_daugter,middle_son,middle_daughter,young_son,young_daugter,old_little_son,old_little_daughter,little_son,little_daughter,middle_little_son,middle_little_daugter") + .OldAnnotation("Npgsql:Enum:gender_type", "male,female") + .OldAnnotation("Npgsql:Enum:hair_color_type", "dark_purple,yellow,blue,purple,orange,brown,green,dark_grey,light_blue,pink_red,light_yellow,light_pink,light_green,light_grey,sky_blue,black,dark_orange,dark_orange_variant2,dark_orange_variant3,dark_orange_variant4,dark_orange_variant5,dark_orange_variant6,light_orange,light_light_orange,light_light_light_orange,light_light_light_light_orange,super_light_orange,dark_yellow,light_light_yellow,kaki_yellow,super_light_yellow,super_light_yellow2,super_light_yellow3,little_dark_yellow,yellow_variant,yellow_variant1,yellow_variant2,yellow_variant3,yellow_variant4,yellow_variant5,yellow_variant6,yellow_variant7,yellow_variant8,yellow_variant9,green_variant,green_variant1,dark_green_variant,green_more_dark_variant,green_variant2,green_variant3,green_variant4,green_variant5,green_variant6,green_variant7,green_variant8,green_variant9,green_variant10,green_variant11,green_variant12,green_variant13,green_variant14,green_variant15,green_variant16,green_variant17,green_variant18,green_variant19,green_variant20,light_blue_variant1,light_blue_variant2,light_blue_variant3,light_blue_variant4,light_blue_variant5,light_blue_variant6,light_blue_variant7,light_blue_variant8,light_blue_variant9,light_blue_variant10,light_blue_variant11,light_blue_variant12,light_blue_variant13,dark_black,light_blue_variant14,light_blue_variant15,light_blue_variant16,light_blue_variant17,blue_variant,blue_variant_dark,blue_variant_dark_dark,blue_variant_dark_dark2,flash_blue,flash_blue_dark,flash_blue_dark2,flash_blue_dark3,flash_blue_dark4,flash_blue_dark5,flash_blue_dark6,flash_blue_dark7,flash_blue_dark8,flash_blue_dark9,white,flash_blue_dark10,flash_blue1,flash_blue2,flash_blue3,flash_blue4,flash_blue5,flash_purple,flash_light_purple,flash_light_purple2,flash_light_purple3,flash_light_purple4,flash_light_purple5,light_purple,purple_variant1,purple_variant2,purple_variant3,purple_variant4,purple_variant5,purple_variant6,purple_variant7,purple_variant8,purple_variant9,purple_variant10,purple_variant11,purple_variant12,purple_variant13,purple_variant14,purple_variant15") + .OldAnnotation("Npgsql:Enum:hair_style_type", "hair_style_a,hair_style_b,hair_style_c,hair_style_d,no_hair") + .OldAnnotation("Npgsql:Enum:item_effect_type", "no_effect,teleport,apply_hair_die,apply_hair_style,speaker,marriage_proposal,undefined,sp_charger,mate_capture,dropped_sp_recharger,premium_sp_recharger,crafted_sp_recharger,specialist_medal,apply_skin_partner,change_gender,point_initialisation,sealed_tarot_card,tarot_card,red_amulet,blue_amulet,reinforcement_amulet,heroic,random_heroic,attack_amulet,defense_amulet,speed_booster,box_effect,vehicle,gold_nos_merchant_upgrade,silver_nos_merchant_upgrade,inventory_upgrade,pet_space_upgrade,pet_basket_upgrade,pet_backpack_upgrade,inventory_ticket_upgrade,buff_potions,marriage_separation") + .OldAnnotation("Npgsql:Enum:item_type", "weapon,armor,fashion,jewelery,specialist,box,shell,main,upgrade,production,map,special,potion,event,title,quest1,sell,food,snack,magical,part,teacher,ammo,quest2,house,garden,minigame,terrace,miniland_theme") + .OldAnnotation("Npgsql:Enum:mate_type", "partner,pet") + .OldAnnotation("Npgsql:Enum:miniland_state", "open,private,lock") + .OldAnnotation("Npgsql:Enum:monster_type", "unknown,partner,npc,well,portal,boss,elite,peapod,special,gem_space_time") + .OldAnnotation("Npgsql:Enum:noscore_pocket_type", "equipment,main,etc,miniland,specialist,costume,wear,mount,raid") + .OldAnnotation("Npgsql:Enum:penalty_type", "muted,banned,block_exp,block_f_exp,block_rep,warning") + .OldAnnotation("Npgsql:Enum:portal_type", "ts_normal,closed,open,miniland,ts_end,ts_end_closed,exit,exit_closed,raid,effect,blue_raid,dark_raid,time_space,shop_teleport,map_portal") + .OldAnnotation("Npgsql:Enum:quest_type", "hunt,special_collect,collect_in_raid,brings,capture_without_getting_the_monster,capture,times_space,product,number_of_kill,target_reput,ts_point,dialog1,collect_in_ts,required,wear,needed,collect,transmit_gold,go_to,collect_map_entity,use,dialog2,un_know,inspect,win_raid,flower_quest") + .OldAnnotation("Npgsql:Enum:region_type", "en,de,fr,it,pl,es,ru,cs,tr") + .OldAnnotation("Npgsql:Enum:scripted_instance_type", "time_space,raid,raid_act4") + .OldAnnotation("Npgsql:Enum:static_bonus_type", "bazaar_medal_gold,bazaar_medal_silver,back_pack,pet_basket,pet_back_pack,inventory_ticket_upgrade") + .OldAnnotation("Npgsql:Enum:teleporter_type", "teleporter,teleporter_on_map") + .OldAnnotation("Npgsql:Enum:warehouse_type", "warehouse,family_ware_house,pet_warehouse"); + } + } +} diff --git a/src/NosCore.Database/Migrations/NosCoreContextModelSnapshot.cs b/src/NosCore.Database/Migrations/NosCoreContextModelSnapshot.cs index 96cc80f00..a7cbadf15 100644 --- a/src/NosCore.Database/Migrations/NosCoreContextModelSnapshot.cs +++ b/src/NosCore.Database/Migrations/NosCoreContextModelSnapshot.cs @@ -35,7 +35,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "gender_type", new[] { "male", "female" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "hair_color_type", new[] { "dark_purple", "yellow", "blue", "purple", "orange", "brown", "green", "dark_grey", "light_blue", "pink_red", "light_yellow", "light_pink", "light_green", "light_grey", "sky_blue", "black", "dark_orange", "dark_orange_variant2", "dark_orange_variant3", "dark_orange_variant4", "dark_orange_variant5", "dark_orange_variant6", "light_orange", "light_light_orange", "light_light_light_orange", "light_light_light_light_orange", "super_light_orange", "dark_yellow", "light_light_yellow", "kaki_yellow", "super_light_yellow", "super_light_yellow2", "super_light_yellow3", "little_dark_yellow", "yellow_variant", "yellow_variant1", "yellow_variant2", "yellow_variant3", "yellow_variant4", "yellow_variant5", "yellow_variant6", "yellow_variant7", "yellow_variant8", "yellow_variant9", "green_variant", "green_variant1", "dark_green_variant", "green_more_dark_variant", "green_variant2", "green_variant3", "green_variant4", "green_variant5", "green_variant6", "green_variant7", "green_variant8", "green_variant9", "green_variant10", "green_variant11", "green_variant12", "green_variant13", "green_variant14", "green_variant15", "green_variant16", "green_variant17", "green_variant18", "green_variant19", "green_variant20", "light_blue_variant1", "light_blue_variant2", "light_blue_variant3", "light_blue_variant4", "light_blue_variant5", "light_blue_variant6", "light_blue_variant7", "light_blue_variant8", "light_blue_variant9", "light_blue_variant10", "light_blue_variant11", "light_blue_variant12", "light_blue_variant13", "dark_black", "light_blue_variant14", "light_blue_variant15", "light_blue_variant16", "light_blue_variant17", "blue_variant", "blue_variant_dark", "blue_variant_dark_dark", "blue_variant_dark_dark2", "flash_blue", "flash_blue_dark", "flash_blue_dark2", "flash_blue_dark3", "flash_blue_dark4", "flash_blue_dark5", "flash_blue_dark6", "flash_blue_dark7", "flash_blue_dark8", "flash_blue_dark9", "white", "flash_blue_dark10", "flash_blue1", "flash_blue2", "flash_blue3", "flash_blue4", "flash_blue5", "flash_purple", "flash_light_purple", "flash_light_purple2", "flash_light_purple3", "flash_light_purple4", "flash_light_purple5", "light_purple", "purple_variant1", "purple_variant2", "purple_variant3", "purple_variant4", "purple_variant5", "purple_variant6", "purple_variant7", "purple_variant8", "purple_variant9", "purple_variant10", "purple_variant11", "purple_variant12", "purple_variant13", "purple_variant14", "purple_variant15" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "hair_style_type", new[] { "hair_style_a", "hair_style_b", "hair_style_c", "hair_style_d", "no_hair" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_effect_type", new[] { "no_effect", "teleport", "apply_hair_die", "apply_hair_style", "speaker", "marriage_proposal", "undefined", "sp_charger", "dropped_sp_recharger", "premium_sp_recharger", "crafted_sp_recharger", "specialist_medal", "apply_skin_partner", "change_gender", "point_initialisation", "sealed_tarot_card", "tarot_card", "red_amulet", "blue_amulet", "reinforcement_amulet", "heroic", "random_heroic", "attack_amulet", "defense_amulet", "speed_booster", "box_effect", "vehicle", "gold_nos_merchant_upgrade", "silver_nos_merchant_upgrade", "inventory_upgrade", "pet_space_upgrade", "pet_basket_upgrade", "pet_backpack_upgrade", "inventory_ticket_upgrade", "buff_potions", "marriage_separation" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_effect_type", new[] { "no_effect", "teleport", "apply_hair_die", "apply_hair_style", "speaker", "marriage_proposal", "undefined", "sp_charger", "mate_capture", "dropped_sp_recharger", "premium_sp_recharger", "crafted_sp_recharger", "specialist_medal", "apply_skin_partner", "change_gender", "point_initialisation", "sealed_tarot_card", "tarot_card", "red_amulet", "blue_amulet", "reinforcement_amulet", "heroic", "random_heroic", "attack_amulet", "defense_amulet", "speed_booster", "box_effect", "vehicle", "gold_nos_merchant_upgrade", "silver_nos_merchant_upgrade", "inventory_upgrade", "pet_space_upgrade", "pet_basket_upgrade", "pet_backpack_upgrade", "inventory_ticket_upgrade", "buff_potions", "marriage_separation" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_type", new[] { "weapon", "armor", "fashion", "jewelery", "specialist", "box", "shell", "main", "upgrade", "production", "map", "special", "potion", "event", "title", "quest1", "sell", "food", "snack", "magical", "part", "teacher", "ammo", "quest2", "house", "garden", "minigame", "terrace", "miniland_theme" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "mate_type", new[] { "partner", "pet" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "miniland_state", new[] { "open", "private", "lock" }); From 66414166e72764db1d8f9e429e42526f64fac89f Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 02:47:38 +1200 Subject: [PATCH 09/14] chore: bump NosCore.Packets to 20.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up the CMapPacket.MapType → IsEntering rename; updates the one call site in MapInstance.GenerateCMap accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) --- Directory.Packages.props | 2 +- .../Services/MapInstanceGenerationService/MapInstance.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b6f685f0f..8b0d705b2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + diff --git a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs index 4ca2e9525..d9b91ad9b 100644 --- a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs +++ b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs @@ -388,7 +388,7 @@ public CMapPacket GenerateCMap(bool enter) { Type = 0, Id = Map.MapId, - MapType = enter + IsEntering = enter }; } From af15a9648045db5ab5785b792c2d77f53346b0b9 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 02:47:38 +1200 Subject: [PATCH 10/14] fix: guard \$Kill against duplicate death events Return early when the character is already dead so the command doesn't emit a second EntityDiedEvent and fire death side-effects twice. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Command/KillCommandPacketHandler.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs b/src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs index 346be3620..cc052ffb9 100644 --- a/src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs +++ b/src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs @@ -17,6 +17,11 @@ public class KillCommandPacketHandler(IMessageBus messageBus) : PacketHandler Date: Sat, 25 Apr 2026 02:47:39 +1200 Subject: [PATCH 11/14] fix: \$ShoutHere treats whitespace-only input as missing Using IsNullOrWhiteSpace routes " " to the help message instead of broadcasting a blank shout to the whole map. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs b/src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs index 7663d8521..ce2878e3d 100644 --- a/src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs +++ b/src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs @@ -18,7 +18,7 @@ public class ShoutHerePacketHandler : PacketHandler, IWorldPack { public override Task ExecuteAsync(ShoutHerePacket packet, ClientSession session) { - if (string.IsNullOrEmpty(packet.Message)) + if (string.IsNullOrWhiteSpace(packet.Message)) { return session.SendPacketAsync(session.Character.GenerateSay(packet.Help(), SayColorType.Yellow)); } From 37ea9411fd3533d6a2d5b6d736d6c1bd8c0c8c30 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 03:00:53 +1200 Subject: [PATCH 12/14] fix: set NsTestPacket.LeadingBlank to empty so serialized wire matches vanosilla MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unset, the string property serializes to "-", producing "NsTeST - 0 …" instead of the expected "NsTeST 0 …" (literal double space) that vanosilla's LoginPacketsExtensions emits. Clients that tolerate the deserialized form still misparse the serialized-from-NosCore form. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/NosCore.GameObject/Services/LoginService/LoginService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NosCore.GameObject/Services/LoginService/LoginService.cs b/src/NosCore.GameObject/Services/LoginService/LoginService.cs index f9e205beb..311697528 100644 --- a/src/NosCore.GameObject/Services/LoginService/LoginService.cs +++ b/src/NosCore.GameObject/Services/LoginService/LoginService.cs @@ -136,6 +136,7 @@ await clientSession.SendPacketAsync(new FailcPacket var i = 1; var nstest = new NsTestPacket { + LeadingBlank = string.Empty, AccountName = username, SubPacket = subpacket, SessionId = clientSession.SessionId, From 62bb2cc69bc6d37c0d6eac5f3369901691e6103e Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 03:32:14 +1200 Subject: [PATCH 13/14] chore: bump NosCore.Packets to 20.0.2 LeadingBlank now defaults to empty string in NsTestPacket (Packets 20.0.1); drop the redundant explicit assignment. Also picks up the AscrPacket leading-blank addition (20.0.2). Co-Authored-By: Claude Opus 4.7 (1M context) --- Directory.Packages.props | 2 +- src/NosCore.GameObject/Services/LoginService/LoginService.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8b0d705b2..8e5c6ce0e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + diff --git a/src/NosCore.GameObject/Services/LoginService/LoginService.cs b/src/NosCore.GameObject/Services/LoginService/LoginService.cs index 311697528..f9e205beb 100644 --- a/src/NosCore.GameObject/Services/LoginService/LoginService.cs +++ b/src/NosCore.GameObject/Services/LoginService/LoginService.cs @@ -136,7 +136,6 @@ await clientSession.SendPacketAsync(new FailcPacket var i = 1; var nstest = new NsTestPacket { - LeadingBlank = string.Empty, AccountName = username, SubPacket = subpacket, SessionId = clientSession.SessionId, From 3effce01a99cc4865803531969bf5b4942f60ede Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sat, 25 Apr 2026 03:34:14 +1200 Subject: [PATCH 14/14] fix: hard-code universal base skills 200 / 201 / 209 on character create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NosCore was seeding character skills purely from WorldConfiguration.BasicSkills[class][origin]. Every character must own the universal Adventurer set — primary (200), secondary (201) and Pet Catcher (209) — but nothing enforced that: a character created from a config where those were missing ended up with no CharacterSkill row for CastId 16, so `u_s 16 3 ` fell through SkillResolver.Resolve → null and BattleService replied with `cancel 2 0`, which is exactly what we hit live. Match vanosilla (Plugins.PacketHandling/Customization/BaseSkill.cs hard-codes the same three) by always inserting 200 / 201 / 209 in CharNewPacketHandler, de-duped against whatever the config-driven BasicSkills list contributes. The config-side entries that used to carry these three are dropped from world.yml since they're now guaranteed by the handler. Class-specific capture variants (235 Swordsman, 237 Mage) are still picked up by SkillService.LearnClassSkillsAsync on JobLevelUp since they're class-bound in the skill DB, not universal. Co-Authored-By: Claude Opus 4.7 (1M context) --- configuration/world.yml | 9 +++++---- .../CharacterScreen/CharNewPacketHandler.cs | 12 +++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/configuration/world.yml b/configuration/world.yml index 43380c922..2ba9a8160 100644 --- a/configuration/world.yml +++ b/configuration/world.yml @@ -211,11 +211,12 @@ BasicEquipments: MartialArtist: Create80: [] BasicSkills: + # 200 / 201 / 209 (Adventurer primary / secondary / Pet Catcher) are seeded + # unconditionally for every new character in CharNewPacketHandler — matches + # vanosilla's hard-coded BaseSkill list — and must not live here, otherwise + # a JSON misedit could create characters without the capture slot working. Adventurer: - CreateAndUpgrade: - - 200 - - 201 - - 209 + CreateAndUpgrade: [] Swordsman: CreateAndUpgrade: [] Create56: diff --git a/src/NosCore.PacketHandlers/CharacterScreen/CharNewPacketHandler.cs b/src/NosCore.PacketHandlers/CharacterScreen/CharNewPacketHandler.cs index 4b5f2c61e..334ae92d1 100644 --- a/src/NosCore.PacketHandlers/CharacterScreen/CharNewPacketHandler.cs +++ b/src/NosCore.PacketHandlers/CharacterScreen/CharNewPacketHandler.cs @@ -139,9 +139,15 @@ public override async Task ExecuteAsync(CharNewPacket packet, ClientSession clie inventory.AddItemToPocket(InventoryItemInstance.Create(itemBuilderService.Create(itemToAdd.VNum, itemToAdd.Amount, itemToAdd.Rare, itemToAdd.Upgrade), chara.CharacterId), itemToAdd.NoscorePocketType); } - var skillsToAdd = ResolvePack(_worldConfiguration.BasicSkills, @class, origin, new List()); - - foreach (var skillToAdd in skillsToAdd) + // Universal base skills every character must own regardless of class / starter + // origin — matches vanosilla's BaseSkill.cs hard-coded list (VNum 200 primary, + // 201 secondary, 209 Pet Catcher). These cannot live in WorldConfiguration.BasicSkills + // because that table is class/origin-specific and would let a misconfigured JSON + // create a character without the capture slot (u_s 16 returns `cancel 2 ...`). + var skillsToAdd = new List { 200, 201, 209 }; + skillsToAdd.AddRange(ResolvePack(_worldConfiguration.BasicSkills, @class, origin, new List())); + + foreach (var skillToAdd in skillsToAdd.Distinct()) { await characterSkillDao.TryInsertOrUpdateAsync(new CharacterSkillDto { CharacterId = chara.CharacterId, SkillVNum = skillToAdd, Id = Guid.NewGuid() });